npcpy 1.2.34__py3-none-any.whl → 1.2.36__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- npcpy/data/audio.py +35 -1
- npcpy/data/load.py +149 -7
- npcpy/data/video.py +72 -0
- npcpy/ft/diff.py +332 -71
- npcpy/gen/image_gen.py +120 -23
- npcpy/gen/ocr.py +187 -0
- npcpy/memory/command_history.py +231 -40
- npcpy/npc_compiler.py +64 -22
- npcpy/serve.py +1712 -607
- {npcpy-1.2.34.dist-info → npcpy-1.2.36.dist-info}/METADATA +1 -1
- {npcpy-1.2.34.dist-info → npcpy-1.2.36.dist-info}/RECORD +14 -13
- {npcpy-1.2.34.dist-info → npcpy-1.2.36.dist-info}/WHEEL +0 -0
- {npcpy-1.2.34.dist-info → npcpy-1.2.36.dist-info}/licenses/LICENSE +0 -0
- {npcpy-1.2.34.dist-info → npcpy-1.2.36.dist-info}/top_level.txt +0 -0
npcpy/serve.py
CHANGED
|
@@ -8,6 +8,10 @@ import sys
|
|
|
8
8
|
import traceback
|
|
9
9
|
import glob
|
|
10
10
|
import re
|
|
11
|
+
import time
|
|
12
|
+
import asyncio
|
|
13
|
+
from typing import Optional, List, Dict, Callable, Any
|
|
14
|
+
from contextlib import AsyncExitStack
|
|
11
15
|
|
|
12
16
|
import io
|
|
13
17
|
from flask_cors import CORS
|
|
@@ -17,6 +21,8 @@ import json
|
|
|
17
21
|
from pathlib import Path
|
|
18
22
|
import yaml
|
|
19
23
|
from dotenv import load_dotenv
|
|
24
|
+
from mcp import ClientSession, StdioServerParameters
|
|
25
|
+
from mcp.client.stdio import stdio_client
|
|
20
26
|
|
|
21
27
|
from PIL import Image
|
|
22
28
|
from PIL import ImageFile
|
|
@@ -43,13 +49,14 @@ from npcpy.memory.knowledge_graph import load_kg_from_db
|
|
|
43
49
|
from npcpy.memory.search import execute_rag_command, execute_brainblast_command
|
|
44
50
|
from npcpy.data.load import load_file_contents
|
|
45
51
|
from npcpy.data.web import search_web
|
|
52
|
+
|
|
46
53
|
from npcsh._state import get_relevant_memories, search_kg_facts
|
|
47
54
|
|
|
48
55
|
import base64
|
|
49
56
|
import shutil
|
|
50
57
|
import uuid
|
|
51
58
|
|
|
52
|
-
from npcpy.llm_funcs import gen_image
|
|
59
|
+
from npcpy.llm_funcs import gen_image, breathe
|
|
53
60
|
|
|
54
61
|
from sqlalchemy import create_engine, text
|
|
55
62
|
from sqlalchemy.orm import sessionmaker
|
|
@@ -60,14 +67,12 @@ from npcpy.memory.command_history import (
|
|
|
60
67
|
save_conversation_message,
|
|
61
68
|
generate_message_id,
|
|
62
69
|
)
|
|
63
|
-
from npcpy.npc_compiler import Jinx, NPC, Team
|
|
70
|
+
from npcpy.npc_compiler import Jinx, NPC, Team, load_jinxs_from_directory, build_jinx_tool_catalog
|
|
64
71
|
|
|
65
72
|
from npcpy.llm_funcs import (
|
|
66
73
|
get_llm_response, check_llm_command
|
|
67
74
|
)
|
|
68
|
-
from
|
|
69
|
-
import base64
|
|
70
|
-
|
|
75
|
+
from termcolor import cprint
|
|
71
76
|
from npcpy.tools import auto_tools
|
|
72
77
|
|
|
73
78
|
import json
|
|
@@ -84,6 +89,235 @@ cancellation_flags = {}
|
|
|
84
89
|
cancellation_lock = threading.Lock()
|
|
85
90
|
|
|
86
91
|
|
|
92
|
+
# Minimal MCP client (inlined from npcsh corca to avoid corca import)
|
|
93
|
+
class MCPClientNPC:
|
|
94
|
+
def __init__(self, debug: bool = True):
|
|
95
|
+
self.debug = debug
|
|
96
|
+
self.session: Optional[ClientSession] = None
|
|
97
|
+
try:
|
|
98
|
+
self._loop = asyncio.get_event_loop()
|
|
99
|
+
if self._loop.is_closed():
|
|
100
|
+
self._loop = asyncio.new_event_loop()
|
|
101
|
+
asyncio.set_event_loop(self._loop)
|
|
102
|
+
except RuntimeError:
|
|
103
|
+
self._loop = asyncio.new_event_loop()
|
|
104
|
+
asyncio.set_event_loop(self._loop)
|
|
105
|
+
self._exit_stack = self._loop.run_until_complete(AsyncExitStack().__aenter__())
|
|
106
|
+
self.available_tools_llm: List[Dict[str, Any]] = []
|
|
107
|
+
self.tool_map: Dict[str, Callable] = {}
|
|
108
|
+
self.server_script_path: Optional[str] = None
|
|
109
|
+
|
|
110
|
+
def _log(self, message: str, color: str = "cyan") -> None:
|
|
111
|
+
if self.debug:
|
|
112
|
+
cprint(f"[MCP Client] {message}", color, file=sys.stderr)
|
|
113
|
+
|
|
114
|
+
async def _connect_async(self, server_script_path: str) -> None:
|
|
115
|
+
self._log(f"Attempting to connect to MCP server: {server_script_path}")
|
|
116
|
+
self.server_script_path = server_script_path
|
|
117
|
+
abs_path = os.path.abspath(server_script_path)
|
|
118
|
+
if not os.path.exists(abs_path):
|
|
119
|
+
raise FileNotFoundError(f"MCP server script not found: {abs_path}")
|
|
120
|
+
|
|
121
|
+
if abs_path.endswith('.py'):
|
|
122
|
+
cmd_parts = [sys.executable, abs_path]
|
|
123
|
+
elif os.access(abs_path, os.X_OK):
|
|
124
|
+
cmd_parts = [abs_path]
|
|
125
|
+
else:
|
|
126
|
+
raise ValueError(f"Unsupported MCP server script type or not executable: {abs_path}")
|
|
127
|
+
|
|
128
|
+
server_params = StdioServerParameters(
|
|
129
|
+
command=cmd_parts[0],
|
|
130
|
+
args=[abs_path],
|
|
131
|
+
env=os.environ.copy(),
|
|
132
|
+
cwd=os.path.dirname(abs_path) or "."
|
|
133
|
+
)
|
|
134
|
+
if self.session:
|
|
135
|
+
await self._exit_stack.aclose()
|
|
136
|
+
|
|
137
|
+
self._exit_stack = AsyncExitStack()
|
|
138
|
+
|
|
139
|
+
stdio_transport = await self._exit_stack.enter_async_context(stdio_client(server_params))
|
|
140
|
+
self.session = await self._exit_stack.enter_async_context(ClientSession(*stdio_transport))
|
|
141
|
+
await self.session.initialize()
|
|
142
|
+
|
|
143
|
+
response = await self.session.list_tools()
|
|
144
|
+
self.available_tools_llm = []
|
|
145
|
+
self.tool_map = {}
|
|
146
|
+
|
|
147
|
+
if response.tools:
|
|
148
|
+
for mcp_tool in response.tools:
|
|
149
|
+
tool_def = {
|
|
150
|
+
"type": "function",
|
|
151
|
+
"function": {
|
|
152
|
+
"name": mcp_tool.name,
|
|
153
|
+
"description": mcp_tool.description or f"MCP tool: {mcp_tool.name}",
|
|
154
|
+
"parameters": getattr(mcp_tool, "inputSchema", {"type": "object", "properties": {}})
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
self.available_tools_llm.append(tool_def)
|
|
158
|
+
|
|
159
|
+
def make_tool_func(tool_name_closure):
|
|
160
|
+
async def tool_func(**kwargs):
|
|
161
|
+
if not self.session:
|
|
162
|
+
return {"error": "No MCP session"}
|
|
163
|
+
self._log(f"About to call MCP tool {tool_name_closure}")
|
|
164
|
+
try:
|
|
165
|
+
cleaned_kwargs = {k: (None if v == 'None' else v) for k, v in kwargs.items()}
|
|
166
|
+
result = await asyncio.wait_for(
|
|
167
|
+
self.session.call_tool(tool_name_closure, cleaned_kwargs),
|
|
168
|
+
timeout=30.0
|
|
169
|
+
)
|
|
170
|
+
self._log(f"MCP tool {tool_name_closure} returned: {type(result)}")
|
|
171
|
+
return result
|
|
172
|
+
except asyncio.TimeoutError:
|
|
173
|
+
self._log(f"Tool {tool_name_closure} timed out after 30 seconds", "red")
|
|
174
|
+
return {"error": f"Tool {tool_name_closure} timed out"}
|
|
175
|
+
except Exception as e:
|
|
176
|
+
self._log(f"Tool {tool_name_closure} error: {e}", "red")
|
|
177
|
+
return {"error": str(e)}
|
|
178
|
+
|
|
179
|
+
def sync_wrapper(**kwargs):
|
|
180
|
+
self._log(f"Sync wrapper called for {tool_name_closure}")
|
|
181
|
+
return self._loop.run_until_complete(tool_func(**kwargs))
|
|
182
|
+
|
|
183
|
+
return sync_wrapper
|
|
184
|
+
|
|
185
|
+
self.tool_map[mcp_tool.name] = make_tool_func(mcp_tool.name)
|
|
186
|
+
tool_names = list(self.tool_map.keys())
|
|
187
|
+
self._log(f"Connection successful. Tools: {', '.join(tool_names) if tool_names else 'None'}")
|
|
188
|
+
|
|
189
|
+
def connect_sync(self, server_script_path: str) -> bool:
|
|
190
|
+
loop = self._loop
|
|
191
|
+
if loop.is_closed():
|
|
192
|
+
self._loop = asyncio.new_event_loop()
|
|
193
|
+
asyncio.set_event_loop(self._loop)
|
|
194
|
+
loop = self._loop
|
|
195
|
+
try:
|
|
196
|
+
loop.run_until_complete(self._connect_async(server_script_path))
|
|
197
|
+
return True
|
|
198
|
+
except Exception as e:
|
|
199
|
+
cprint(f"MCP connection failed: {e}", "red", file=sys.stderr)
|
|
200
|
+
return False
|
|
201
|
+
|
|
202
|
+
def disconnect_sync(self):
|
|
203
|
+
if self.session:
|
|
204
|
+
self._log("Disconnecting MCP session.")
|
|
205
|
+
loop = self._loop
|
|
206
|
+
if not loop.is_closed():
|
|
207
|
+
try:
|
|
208
|
+
async def close_session():
|
|
209
|
+
await self.session.close()
|
|
210
|
+
await self._exit_stack.aclose()
|
|
211
|
+
loop.run_until_complete(close_session())
|
|
212
|
+
except RuntimeError:
|
|
213
|
+
pass
|
|
214
|
+
except Exception as e:
|
|
215
|
+
print(f"Error during MCP client disconnect: {e}", file=sys.stderr)
|
|
216
|
+
self.session = None
|
|
217
|
+
self._exit_stack = None
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def get_llm_response_with_handling(prompt, npc, messages, tools, stream, team, context=None):
|
|
221
|
+
"""Unified LLM response with basic exception handling (inlined from corca to avoid that dependency)."""
|
|
222
|
+
try:
|
|
223
|
+
return get_llm_response(
|
|
224
|
+
prompt=prompt,
|
|
225
|
+
npc=npc,
|
|
226
|
+
messages=messages,
|
|
227
|
+
tools=tools,
|
|
228
|
+
auto_process_tool_calls=False,
|
|
229
|
+
stream=stream,
|
|
230
|
+
team=team,
|
|
231
|
+
context=context
|
|
232
|
+
)
|
|
233
|
+
except Exception:
|
|
234
|
+
# Fallback retry without context compression logic to keep it simple here.
|
|
235
|
+
return get_llm_response(
|
|
236
|
+
prompt=prompt,
|
|
237
|
+
npc=npc,
|
|
238
|
+
messages=messages,
|
|
239
|
+
tools=tools,
|
|
240
|
+
auto_process_tool_calls=False,
|
|
241
|
+
stream=stream,
|
|
242
|
+
team=team,
|
|
243
|
+
context=context
|
|
244
|
+
)
|
|
245
|
+
class MCPServerManager:
|
|
246
|
+
"""
|
|
247
|
+
Simple in-process tracker for launching/stopping MCP servers.
|
|
248
|
+
Currently uses subprocess.Popen to start a Python stdio MCP server script.
|
|
249
|
+
"""
|
|
250
|
+
|
|
251
|
+
def __init__(self):
|
|
252
|
+
self._procs = {}
|
|
253
|
+
self._lock = threading.Lock()
|
|
254
|
+
|
|
255
|
+
def start(self, server_path: str):
|
|
256
|
+
server_path = os.path.expanduser(server_path)
|
|
257
|
+
abs_path = os.path.abspath(server_path)
|
|
258
|
+
if not os.path.exists(abs_path):
|
|
259
|
+
raise FileNotFoundError(f"MCP server script not found at {abs_path}")
|
|
260
|
+
|
|
261
|
+
with self._lock:
|
|
262
|
+
existing = self._procs.get(abs_path)
|
|
263
|
+
if existing and existing.poll() is None:
|
|
264
|
+
return {"status": "running", "pid": existing.pid, "serverPath": abs_path}
|
|
265
|
+
|
|
266
|
+
cmd = [sys.executable, abs_path]
|
|
267
|
+
proc = subprocess.Popen(
|
|
268
|
+
cmd,
|
|
269
|
+
cwd=os.path.dirname(abs_path) or ".",
|
|
270
|
+
stdout=subprocess.PIPE,
|
|
271
|
+
stderr=subprocess.PIPE,
|
|
272
|
+
)
|
|
273
|
+
self._procs[abs_path] = proc
|
|
274
|
+
return {"status": "started", "pid": proc.pid, "serverPath": abs_path}
|
|
275
|
+
|
|
276
|
+
def stop(self, server_path: str):
|
|
277
|
+
server_path = os.path.expanduser(server_path)
|
|
278
|
+
abs_path = os.path.abspath(server_path)
|
|
279
|
+
with self._lock:
|
|
280
|
+
proc = self._procs.get(abs_path)
|
|
281
|
+
if not proc:
|
|
282
|
+
return {"status": "not_found", "serverPath": abs_path}
|
|
283
|
+
if proc.poll() is None:
|
|
284
|
+
proc.terminate()
|
|
285
|
+
try:
|
|
286
|
+
proc.wait(timeout=5)
|
|
287
|
+
except subprocess.TimeoutExpired:
|
|
288
|
+
proc.kill()
|
|
289
|
+
del self._procs[abs_path]
|
|
290
|
+
return {"status": "stopped", "serverPath": abs_path}
|
|
291
|
+
|
|
292
|
+
def status(self, server_path: str):
|
|
293
|
+
server_path = os.path.expanduser(server_path)
|
|
294
|
+
abs_path = os.path.abspath(server_path)
|
|
295
|
+
with self._lock:
|
|
296
|
+
proc = self._procs.get(abs_path)
|
|
297
|
+
if not proc:
|
|
298
|
+
return {"status": "not_started", "serverPath": abs_path}
|
|
299
|
+
running = proc.poll() is None
|
|
300
|
+
return {
|
|
301
|
+
"status": "running" if running else "exited",
|
|
302
|
+
"serverPath": abs_path,
|
|
303
|
+
"pid": proc.pid,
|
|
304
|
+
"returncode": None if running else proc.returncode,
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
def running(self):
|
|
308
|
+
with self._lock:
|
|
309
|
+
return {
|
|
310
|
+
path: {
|
|
311
|
+
"pid": proc.pid,
|
|
312
|
+
"status": "running" if proc.poll() is None else "exited",
|
|
313
|
+
"returncode": None if proc.poll() is None else proc.returncode,
|
|
314
|
+
}
|
|
315
|
+
for path, proc in self._procs.items()
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
mcp_server_manager = MCPServerManager()
|
|
320
|
+
|
|
87
321
|
def get_project_npc_directory(current_path=None):
|
|
88
322
|
"""
|
|
89
323
|
Get the project NPC directory based on the current path
|
|
@@ -186,6 +420,34 @@ def get_db_session():
|
|
|
186
420
|
Session = sessionmaker(bind=engine)
|
|
187
421
|
return Session()
|
|
188
422
|
|
|
423
|
+
|
|
424
|
+
def resolve_mcp_server_path(current_path=None, explicit_path=None, force_global=False):
|
|
425
|
+
"""
|
|
426
|
+
Resolve an MCP server path using npcsh.corca's helper when available.
|
|
427
|
+
Falls back to ~/.npcsh/npc_team/mcp_server.py.
|
|
428
|
+
"""
|
|
429
|
+
if explicit_path:
|
|
430
|
+
abs_path = os.path.abspath(os.path.expanduser(explicit_path))
|
|
431
|
+
if os.path.exists(abs_path):
|
|
432
|
+
return abs_path
|
|
433
|
+
try:
|
|
434
|
+
from npcsh.corca import _resolve_and_copy_mcp_server_path
|
|
435
|
+
resolved = _resolve_and_copy_mcp_server_path(
|
|
436
|
+
explicit_path=explicit_path,
|
|
437
|
+
current_path=current_path,
|
|
438
|
+
team_ctx_mcp_servers=None,
|
|
439
|
+
interactive=False,
|
|
440
|
+
auto_copy_bypass=True,
|
|
441
|
+
force_global=force_global,
|
|
442
|
+
)
|
|
443
|
+
if resolved:
|
|
444
|
+
return os.path.abspath(resolved)
|
|
445
|
+
except Exception as e:
|
|
446
|
+
print(f"resolve_mcp_server_path: fallback path due to error: {e}")
|
|
447
|
+
|
|
448
|
+
fallback = os.path.expanduser("~/.npcsh/npc_team/mcp_server.py")
|
|
449
|
+
return fallback
|
|
450
|
+
|
|
189
451
|
extension_map = {
|
|
190
452
|
"PNG": "images",
|
|
191
453
|
"JPG": "images",
|
|
@@ -441,8 +703,6 @@ def capture():
|
|
|
441
703
|
return None
|
|
442
704
|
|
|
443
705
|
return jsonify({"screenshot": screenshot})
|
|
444
|
-
|
|
445
|
-
|
|
446
706
|
@app.route("/api/settings/global", methods=["GET", "OPTIONS"])
|
|
447
707
|
def get_global_settings():
|
|
448
708
|
if request.method == "OPTIONS":
|
|
@@ -451,22 +711,22 @@ def get_global_settings():
|
|
|
451
711
|
try:
|
|
452
712
|
npcshrc_path = os.path.expanduser("~/.npcshrc")
|
|
453
713
|
|
|
454
|
-
|
|
455
714
|
global_settings = {
|
|
456
715
|
"model": "llama3.2",
|
|
457
716
|
"provider": "ollama",
|
|
458
717
|
"embedding_model": "nomic-embed-text",
|
|
459
718
|
"embedding_provider": "ollama",
|
|
460
719
|
"search_provider": "perplexity",
|
|
461
|
-
"NPC_STUDIO_LICENSE_KEY": "",
|
|
462
720
|
"default_folder": os.path.expanduser("~/.npcsh/"),
|
|
721
|
+
"is_predictive_text_enabled": False, # Default value for the new setting
|
|
722
|
+
"predictive_text_model": "llama3.2", # Default predictive text model
|
|
723
|
+
"predictive_text_provider": "ollama", # Default predictive text provider
|
|
463
724
|
}
|
|
464
725
|
global_vars = {}
|
|
465
726
|
|
|
466
727
|
if os.path.exists(npcshrc_path):
|
|
467
728
|
with open(npcshrc_path, "r") as f:
|
|
468
729
|
for line in f:
|
|
469
|
-
|
|
470
730
|
line = line.split("#")[0].strip()
|
|
471
731
|
if not line:
|
|
472
732
|
continue
|
|
@@ -474,33 +734,35 @@ def get_global_settings():
|
|
|
474
734
|
if "=" not in line:
|
|
475
735
|
continue
|
|
476
736
|
|
|
477
|
-
|
|
478
737
|
key, value = line.split("=", 1)
|
|
479
738
|
key = key.strip()
|
|
480
739
|
if key.startswith("export "):
|
|
481
740
|
key = key[7:]
|
|
482
741
|
|
|
483
|
-
|
|
484
742
|
value = value.strip()
|
|
485
743
|
if value.startswith('"') and value.endswith('"'):
|
|
486
744
|
value = value[1:-1]
|
|
487
745
|
elif value.startswith("'") and value.endswith("'"):
|
|
488
746
|
value = value[1:-1]
|
|
489
747
|
|
|
490
|
-
|
|
491
748
|
key_mapping = {
|
|
492
749
|
"NPCSH_MODEL": "model",
|
|
493
750
|
"NPCSH_PROVIDER": "provider",
|
|
494
751
|
"NPCSH_EMBEDDING_MODEL": "embedding_model",
|
|
495
752
|
"NPCSH_EMBEDDING_PROVIDER": "embedding_provider",
|
|
496
753
|
"NPCSH_SEARCH_PROVIDER": "search_provider",
|
|
497
|
-
"NPC_STUDIO_LICENSE_KEY": "NPC_STUDIO_LICENSE_KEY",
|
|
498
754
|
"NPCSH_STREAM_OUTPUT": "NPCSH_STREAM_OUTPUT",
|
|
499
755
|
"NPC_STUDIO_DEFAULT_FOLDER": "default_folder",
|
|
756
|
+
"NPC_STUDIO_PREDICTIVE_TEXT_ENABLED": "is_predictive_text_enabled", # New mapping
|
|
757
|
+
"NPC_STUDIO_PREDICTIVE_TEXT_MODEL": "predictive_text_model", # New mapping
|
|
758
|
+
"NPC_STUDIO_PREDICTIVE_TEXT_PROVIDER": "predictive_text_provider", # New mapping
|
|
500
759
|
}
|
|
501
760
|
|
|
502
761
|
if key in key_mapping:
|
|
503
|
-
|
|
762
|
+
if key == "NPC_STUDIO_PREDICTIVE_TEXT_ENABLED":
|
|
763
|
+
global_settings[key_mapping[key]] = value.lower() == 'true'
|
|
764
|
+
else:
|
|
765
|
+
global_settings[key_mapping[key]] = value
|
|
504
766
|
else:
|
|
505
767
|
global_vars[key] = value
|
|
506
768
|
|
|
@@ -517,6 +779,7 @@ def get_global_settings():
|
|
|
517
779
|
except Exception as e:
|
|
518
780
|
print(f"Error in get_global_settings: {str(e)}")
|
|
519
781
|
return jsonify({"error": str(e)}), 500
|
|
782
|
+
|
|
520
783
|
def _get_jinx_files_recursively(directory):
|
|
521
784
|
"""Helper to recursively find all .jinx file paths."""
|
|
522
785
|
jinx_paths = []
|
|
@@ -550,58 +813,7 @@ def get_available_jinxs():
|
|
|
550
813
|
traceback.print_exc()
|
|
551
814
|
return jsonify({'jinxs': [], 'error': str(e)}), 500
|
|
552
815
|
|
|
553
|
-
|
|
554
|
-
def get_global_jinxs():
|
|
555
|
-
global_jinxs_dir = os.path.expanduser('~/.npcsh/npc_team/jinxs')
|
|
556
|
-
|
|
557
|
-
# Directories to exclude entirely
|
|
558
|
-
excluded_dirs = ['core', 'npc_studio']
|
|
559
|
-
|
|
560
|
-
code_jinxs = []
|
|
561
|
-
mode_jinxs = []
|
|
562
|
-
util_jinxs = []
|
|
563
|
-
|
|
564
|
-
if os.path.exists(global_jinxs_dir):
|
|
565
|
-
for root, dirs, files in os.walk(global_jinxs_dir):
|
|
566
|
-
# Filter out excluded directories
|
|
567
|
-
dirs[:] = [d for d in dirs if d not in excluded_dirs]
|
|
568
|
-
|
|
569
|
-
for filename in files:
|
|
570
|
-
if filename.endswith('.jinx'):
|
|
571
|
-
try:
|
|
572
|
-
jinx_path = os.path.join(root, filename)
|
|
573
|
-
with open(jinx_path, 'r') as f:
|
|
574
|
-
jinx_data = yaml.safe_load(f)
|
|
575
|
-
|
|
576
|
-
if jinx_data:
|
|
577
|
-
jinx_name = jinx_data.get('jinx_name', filename[:-5])
|
|
578
|
-
|
|
579
|
-
jinx_obj = {
|
|
580
|
-
'name': jinx_name,
|
|
581
|
-
'display_name': jinx_data.get('description', jinx_name),
|
|
582
|
-
'description': jinx_data.get('description', ''),
|
|
583
|
-
'inputs': jinx_data.get('inputs', []),
|
|
584
|
-
'path': jinx_path
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
# Categorize based on directory
|
|
588
|
-
rel_path = os.path.relpath(root, global_jinxs_dir)
|
|
589
|
-
|
|
590
|
-
if rel_path.startswith('code'):
|
|
591
|
-
code_jinxs.append(jinx_obj)
|
|
592
|
-
elif rel_path.startswith('modes'):
|
|
593
|
-
mode_jinxs.append(jinx_obj)
|
|
594
|
-
elif rel_path.startswith('utils'):
|
|
595
|
-
util_jinxs.append(jinx_obj)
|
|
596
|
-
|
|
597
|
-
except Exception as e:
|
|
598
|
-
print(f"Error loading jinx {filename}: {e}")
|
|
599
|
-
|
|
600
|
-
return jsonify({
|
|
601
|
-
'code': code_jinxs,
|
|
602
|
-
'modes': mode_jinxs,
|
|
603
|
-
'utils': util_jinxs
|
|
604
|
-
})
|
|
816
|
+
|
|
605
817
|
@app.route("/api/jinx/execute", methods=["POST"])
|
|
606
818
|
def execute_jinx():
|
|
607
819
|
"""
|
|
@@ -823,8 +1035,6 @@ def execute_jinx():
|
|
|
823
1035
|
return Response(final_output_string, mimetype="text/html")
|
|
824
1036
|
else:
|
|
825
1037
|
return Response(final_output_string, mimetype="text/plain")
|
|
826
|
-
|
|
827
|
-
|
|
828
1038
|
@app.route("/api/settings/global", methods=["POST", "OPTIONS"])
|
|
829
1039
|
def save_global_settings():
|
|
830
1040
|
if request.method == "OPTIONS":
|
|
@@ -840,35 +1050,41 @@ def save_global_settings():
|
|
|
840
1050
|
"embedding_model": "NPCSH_EMBEDDING_MODEL",
|
|
841
1051
|
"embedding_provider": "NPCSH_EMBEDDING_PROVIDER",
|
|
842
1052
|
"search_provider": "NPCSH_SEARCH_PROVIDER",
|
|
843
|
-
"NPC_STUDIO_LICENSE_KEY": "NPC_STUDIO_LICENSE_KEY",
|
|
844
1053
|
"NPCSH_STREAM_OUTPUT": "NPCSH_STREAM_OUTPUT",
|
|
845
1054
|
"default_folder": "NPC_STUDIO_DEFAULT_FOLDER",
|
|
1055
|
+
"is_predictive_text_enabled": "NPC_STUDIO_PREDICTIVE_TEXT_ENABLED", # New mapping
|
|
1056
|
+
"predictive_text_model": "NPC_STUDIO_PREDICTIVE_TEXT_MODEL", # New mapping
|
|
1057
|
+
"predictive_text_provider": "NPC_STUDIO_PREDICTIVE_TEXT_PROVIDER", # New mapping
|
|
846
1058
|
}
|
|
847
1059
|
|
|
848
1060
|
os.makedirs(os.path.dirname(npcshrc_path), exist_ok=True)
|
|
849
1061
|
print(data)
|
|
850
1062
|
with open(npcshrc_path, "w") as f:
|
|
851
|
-
|
|
1063
|
+
|
|
852
1064
|
for key, value in data.get("global_settings", {}).items():
|
|
853
|
-
if key in key_mapping and value:
|
|
854
|
-
|
|
855
|
-
if
|
|
856
|
-
|
|
857
|
-
|
|
1065
|
+
if key in key_mapping and value is not None: # Check for None explicitly
|
|
1066
|
+
# Handle boolean conversion for saving
|
|
1067
|
+
if key == "is_predictive_text_enabled":
|
|
1068
|
+
value_to_write = str(value).upper()
|
|
1069
|
+
elif " " in str(value):
|
|
1070
|
+
value_to_write = f'"{value}"'
|
|
1071
|
+
else:
|
|
1072
|
+
value_to_write = str(value)
|
|
1073
|
+
f.write(f"export {key_mapping[key]}={value_to_write}\n")
|
|
858
1074
|
|
|
859
|
-
|
|
860
1075
|
for key, value in data.get("global_vars", {}).items():
|
|
861
|
-
if key and value:
|
|
1076
|
+
if key and value is not None: # Check for None explicitly
|
|
862
1077
|
if " " in str(value):
|
|
863
|
-
|
|
864
|
-
|
|
1078
|
+
value_to_write = f'"{value}"'
|
|
1079
|
+
else:
|
|
1080
|
+
value_to_write = str(value)
|
|
1081
|
+
f.write(f"export {key}={value_to_write}\n")
|
|
865
1082
|
|
|
866
1083
|
return jsonify({"message": "Global settings saved successfully", "error": None})
|
|
867
1084
|
|
|
868
1085
|
except Exception as e:
|
|
869
1086
|
print(f"Error in save_global_settings: {str(e)}")
|
|
870
1087
|
return jsonify({"error": str(e)}), 500
|
|
871
|
-
|
|
872
1088
|
@app.route("/api/settings/project", methods=["GET", "OPTIONS"])
|
|
873
1089
|
def get_project_settings():
|
|
874
1090
|
if request.method == "OPTIONS":
|
|
@@ -1050,8 +1266,542 @@ def save_jinx():
|
|
|
1050
1266
|
return jsonify({"status": "success"})
|
|
1051
1267
|
except Exception as e:
|
|
1052
1268
|
return jsonify({"error": str(e)}), 500
|
|
1269
|
+
def serialize_jinx_inputs(inputs):
|
|
1270
|
+
result = []
|
|
1271
|
+
for inp in inputs:
|
|
1272
|
+
if isinstance(inp, str):
|
|
1273
|
+
result.append(inp)
|
|
1274
|
+
elif isinstance(inp, dict):
|
|
1275
|
+
key = list(inp.keys())[0]
|
|
1276
|
+
result.append(key)
|
|
1277
|
+
else:
|
|
1278
|
+
result.append(str(inp))
|
|
1279
|
+
return result
|
|
1280
|
+
|
|
1281
|
+
@app.route("/api/jinx/test", methods=["POST"])
|
|
1282
|
+
def test_jinx():
|
|
1283
|
+
data = request.json
|
|
1284
|
+
jinx_data = data.get("jinx")
|
|
1285
|
+
test_inputs = data.get("inputs", {})
|
|
1286
|
+
current_path = data.get("currentPath")
|
|
1287
|
+
|
|
1288
|
+
if current_path:
|
|
1289
|
+
load_project_env(current_path)
|
|
1290
|
+
|
|
1291
|
+
jinx = Jinx(jinx_data=jinx_data)
|
|
1292
|
+
|
|
1293
|
+
from jinja2 import Environment
|
|
1294
|
+
temp_env = Environment()
|
|
1295
|
+
jinx.render_first_pass(temp_env, {})
|
|
1296
|
+
|
|
1297
|
+
conversation_id = f"jinx_test_{uuid.uuid4().hex[:8]}"
|
|
1298
|
+
command_history = CommandHistory(app.config.get('DB_PATH'))
|
|
1299
|
+
|
|
1300
|
+
# 1. Save user's test command to conversation_history to get a message_id
|
|
1301
|
+
user_test_command = f"Testing jinx /{jinx.jinx_name} with inputs: {test_inputs}"
|
|
1302
|
+
user_message_id = generate_message_id()
|
|
1303
|
+
save_conversation_message(
|
|
1304
|
+
command_history,
|
|
1305
|
+
conversation_id,
|
|
1306
|
+
"user",
|
|
1307
|
+
user_test_command,
|
|
1308
|
+
wd=current_path,
|
|
1309
|
+
model=None, # Or appropriate model/provider for the test context
|
|
1310
|
+
provider=None,
|
|
1311
|
+
npc=None,
|
|
1312
|
+
message_id=user_message_id
|
|
1313
|
+
)
|
|
1314
|
+
|
|
1315
|
+
# Jinx execution status and output are now part of the assistant's response
|
|
1316
|
+
jinx_execution_status = "success"
|
|
1317
|
+
jinx_error_message = None
|
|
1318
|
+
output = "Jinx execution did not complete." # Default output
|
|
1319
|
+
|
|
1320
|
+
try:
|
|
1321
|
+
result = jinx.execute(
|
|
1322
|
+
input_values=test_inputs,
|
|
1323
|
+
npc=None,
|
|
1324
|
+
messages=[],
|
|
1325
|
+
extra_globals={},
|
|
1326
|
+
jinja_env=temp_env
|
|
1327
|
+
)
|
|
1328
|
+
output = result.get('output', str(result))
|
|
1329
|
+
if result.get('error'): # Assuming jinx.execute might return an 'error' key
|
|
1330
|
+
jinx_execution_status = "failed"
|
|
1331
|
+
jinx_error_message = str(result.get('error'))
|
|
1332
|
+
except Exception as e:
|
|
1333
|
+
jinx_execution_status = "failed"
|
|
1334
|
+
jinx_error_message = str(e)
|
|
1335
|
+
output = f"Jinx execution failed: {e}"
|
|
1336
|
+
|
|
1337
|
+
# The jinx_executions table is populated by a trigger from conversation_history.
|
|
1338
|
+
# The details of the execution (inputs, output, status) are now expected to be
|
|
1339
|
+
# derived by analyzing the user's command and the subsequent assistant's response.
|
|
1340
|
+
# No explicit update to jinx_executions is needed here.
|
|
1341
|
+
|
|
1342
|
+
# 2. Save assistant's response to conversation_history
|
|
1343
|
+
assistant_response_message_id = generate_message_id() # ID for the assistant's response
|
|
1344
|
+
save_conversation_message(
|
|
1345
|
+
command_history,
|
|
1346
|
+
conversation_id,
|
|
1347
|
+
"assistant",
|
|
1348
|
+
output, # The jinx output is the assistant's response for the test
|
|
1349
|
+
wd=current_path,
|
|
1350
|
+
model=None,
|
|
1351
|
+
provider=None,
|
|
1352
|
+
npc=None,
|
|
1353
|
+
message_id=assistant_response_message_id
|
|
1354
|
+
)
|
|
1355
|
+
|
|
1356
|
+
return jsonify({
|
|
1357
|
+
"output": output,
|
|
1358
|
+
"conversation_id": conversation_id,
|
|
1359
|
+
"execution_id": user_message_id, # Return the user's message_id as the execution_id
|
|
1360
|
+
"error": jinx_error_message
|
|
1361
|
+
})
|
|
1362
|
+
from npcpy.ft.diff import train_diffusion, DiffusionConfig
|
|
1363
|
+
import threading
|
|
1364
|
+
|
|
1365
|
+
from npcpy.memory.knowledge_graph import (
|
|
1366
|
+
load_kg_from_db,
|
|
1367
|
+
save_kg_to_db # ADD THIS LINE to import the correct function
|
|
1368
|
+
)
|
|
1369
|
+
|
|
1370
|
+
from collections import defaultdict # ADD THIS LINE for collecting links if not already present
|
|
1371
|
+
|
|
1372
|
+
finetune_jobs = {}
|
|
1373
|
+
|
|
1374
|
+
def extract_and_store_memories(
|
|
1375
|
+
conversation_text,
|
|
1376
|
+
conversation_id,
|
|
1377
|
+
command_history,
|
|
1378
|
+
npc_name,
|
|
1379
|
+
team_name,
|
|
1380
|
+
current_path,
|
|
1381
|
+
model,
|
|
1382
|
+
provider,
|
|
1383
|
+
npc_object=None
|
|
1384
|
+
):
|
|
1385
|
+
from npcpy.llm_funcs import get_facts
|
|
1386
|
+
from npcpy.memory.command_history import format_memory_context
|
|
1387
|
+
# Your CommandHistory.get_memory_examples_for_context returns a dict with 'approved' and 'rejected'
|
|
1388
|
+
memory_examples_dict = command_history.get_memory_examples_for_context(
|
|
1389
|
+
npc=npc_name,
|
|
1390
|
+
team=team_name,
|
|
1391
|
+
directory_path=current_path
|
|
1392
|
+
)
|
|
1393
|
+
|
|
1394
|
+
memory_context = format_memory_context(memory_examples_dict)
|
|
1395
|
+
|
|
1396
|
+
facts = get_facts(
|
|
1397
|
+
conversation_text,
|
|
1398
|
+
model=npc_object.model if npc_object else model,
|
|
1399
|
+
provider=npc_object.provider if npc_object else provider,
|
|
1400
|
+
npc=npc_object,
|
|
1401
|
+
context=memory_context
|
|
1402
|
+
)
|
|
1403
|
+
|
|
1404
|
+
memories_for_approval = []
|
|
1405
|
+
|
|
1406
|
+
# Initialize structures to collect KG data for a single save_kg_to_db call
|
|
1407
|
+
kg_facts_to_save = []
|
|
1408
|
+
kg_concepts_to_save = []
|
|
1409
|
+
fact_to_concept_links_temp = defaultdict(list)
|
|
1410
|
+
|
|
1411
|
+
|
|
1412
|
+
if facts:
|
|
1413
|
+
for i, fact in enumerate(facts):
|
|
1414
|
+
# Store memory in memory_lifecycle table
|
|
1415
|
+
memory_id = command_history.add_memory_to_database(
|
|
1416
|
+
message_id=f"{conversation_id}_{datetime.datetime.now().strftime('%H%M%S')}_{i}",
|
|
1417
|
+
conversation_id=conversation_id,
|
|
1418
|
+
npc=npc_name or "default",
|
|
1419
|
+
team=team_name or "default",
|
|
1420
|
+
directory_path=current_path or "/",
|
|
1421
|
+
initial_memory=fact.get('statement', str(fact)),
|
|
1422
|
+
status="pending_approval",
|
|
1423
|
+
model=npc_object.model if npc_object else model,
|
|
1424
|
+
provider=npc_object.provider if npc_object else provider,
|
|
1425
|
+
final_memory=None # Explicitly None for pending memories
|
|
1426
|
+
)
|
|
1427
|
+
|
|
1428
|
+
memories_for_approval.append({
|
|
1429
|
+
"memory_id": memory_id,
|
|
1430
|
+
"content": fact.get('statement', str(fact)),
|
|
1431
|
+
"type": fact.get('type', 'unknown'),
|
|
1432
|
+
"context": fact.get('source_text', ''),
|
|
1433
|
+
"npc": npc_name or "default"
|
|
1434
|
+
})
|
|
1435
|
+
|
|
1436
|
+
# Collect facts and concepts for the Knowledge Graph
|
|
1437
|
+
#if fact.get('type') == 'concept':
|
|
1438
|
+
# kg_concepts_to_save.append({
|
|
1439
|
+
# "name": fact.get('statement'),
|
|
1440
|
+
# "generation": current_kg_generation,
|
|
1441
|
+
# "origin": "organic" # Assuming 'organic' for extracted facts
|
|
1442
|
+
# })
|
|
1443
|
+
#else: # It's a fact (or unknown type, treat as fact for KG)
|
|
1444
|
+
# kg_facts_to_save.append({
|
|
1445
|
+
# "statement": fact.get('statement'),
|
|
1446
|
+
# "source_text": fact.get('source_text', conversation_text), # Use source_text if available, else conversation_text
|
|
1447
|
+
# "type": fact.get('type', 'fact'), # Default to 'fact' if type is unknown
|
|
1448
|
+
# "generation": current_kg_generation,
|
|
1449
|
+
# "origin": "organic"
|
|
1450
|
+
# })
|
|
1451
|
+
# if fact.get('concepts'): # If this fact has related concepts
|
|
1452
|
+
# for concept_name in fact.get('concepts'):
|
|
1453
|
+
# fact_to_concept_links_temp[fact.get('statement')].append(concept_name)
|
|
1454
|
+
|
|
1455
|
+
# After processing all facts, save them to the KG database in one go
|
|
1456
|
+
if kg_facts_to_save or kg_concepts_to_save:
|
|
1457
|
+
temp_kg_data = {
|
|
1458
|
+
"facts": kg_facts_to_save,
|
|
1459
|
+
"concepts": kg_concepts_to_save,
|
|
1460
|
+
"generation": current_kg_generation,
|
|
1461
|
+
"fact_to_concept_links": fact_to_concept_links_temp,
|
|
1462
|
+
"concept_links": [], # Assuming no concept-to-concept links from direct extraction
|
|
1463
|
+
"fact_to_fact_links": [] # Assuming no fact-to-fact links from direct extraction
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
# Get the SQLAlchemy engine using your existing helper function
|
|
1467
|
+
db_engine = get_db_connection(app.config.get('DB_PATH'))
|
|
1468
|
+
|
|
1469
|
+
# Call the existing save_kg_to_db function
|
|
1470
|
+
save_kg_to_db(
|
|
1471
|
+
engine=db_engine,
|
|
1472
|
+
kg_data=temp_kg_data,
|
|
1473
|
+
team_name=team_name or "default",
|
|
1474
|
+
npc_name=npc_name or "default",
|
|
1475
|
+
directory_path=current_path or "/"
|
|
1476
|
+
)
|
|
1477
|
+
|
|
1478
|
+
return memories_for_approval
|
|
1479
|
+
@app.route('/api/finetuned_models', methods=['GET'])
|
|
1480
|
+
def get_finetuned_models():
|
|
1481
|
+
current_path = request.args.get("currentPath")
|
|
1482
|
+
|
|
1483
|
+
# Define a list of potential root directories where fine-tuned models might be saved.
|
|
1484
|
+
# We'll be very generous here, including both 'models' and 'images' directories
|
|
1485
|
+
# at both global and project levels, as the user's logs indicate saving to 'images'.
|
|
1486
|
+
potential_root_paths = [
|
|
1487
|
+
os.path.expanduser('~/.npcsh/models'), # Standard global models directory
|
|
1488
|
+
os.path.expanduser('~/.npcsh/images'), # Global images directory (where user's model was saved)
|
|
1489
|
+
]
|
|
1490
|
+
if current_path:
|
|
1491
|
+
# Add project-specific model directories if a current_path is provided
|
|
1492
|
+
project_models_path = os.path.join(current_path, 'models')
|
|
1493
|
+
project_images_path = os.path.join(current_path, 'images') # Also check project images directory
|
|
1494
|
+
potential_root_paths.extend([project_models_path, project_images_path])
|
|
1495
|
+
|
|
1496
|
+
finetuned_models = []
|
|
1497
|
+
|
|
1498
|
+
print(f"🌋 Searching for fine-tuned models in potential root paths: {set(potential_root_paths)}") # Use set for unique paths
|
|
1499
|
+
|
|
1500
|
+
for root_path in set(potential_root_paths): # Iterate through unique potential root paths
|
|
1501
|
+
if not os.path.exists(root_path) or not os.path.isdir(root_path):
|
|
1502
|
+
print(f"🌋 Skipping non-existent or non-directory root path: {root_path}")
|
|
1503
|
+
continue
|
|
1504
|
+
|
|
1505
|
+
print(f"🌋 Scanning root path: {root_path}")
|
|
1506
|
+
for model_dir_name in os.listdir(root_path):
|
|
1507
|
+
full_model_path = os.path.join(root_path, model_dir_name)
|
|
1508
|
+
|
|
1509
|
+
if not os.path.isdir(full_model_path):
|
|
1510
|
+
print(f"🌋 Skipping {full_model_path}: Not a directory.")
|
|
1511
|
+
continue
|
|
1512
|
+
|
|
1513
|
+
# NEW STRATEGY: Check for user's specific output files
|
|
1514
|
+
# Look for 'model_final.pt' or the 'checkpoints' directory
|
|
1515
|
+
has_model_final_pt = os.path.exists(os.path.join(full_model_path, 'model_final.pt'))
|
|
1516
|
+
has_checkpoints_dir = os.path.isdir(os.path.join(full_model_path, 'checkpoints'))
|
|
1517
|
+
|
|
1518
|
+
if has_model_final_pt or has_checkpoints_dir:
|
|
1519
|
+
print(f"🌋 Identified fine-tuned model: {model_dir_name} at {full_model_path} (found model_final.pt or checkpoints dir)")
|
|
1520
|
+
finetuned_models.append({
|
|
1521
|
+
"value": full_model_path, # This is the path to the directory containing the .pt files
|
|
1522
|
+
"provider": "diffusers", # Provider is still "diffusers"
|
|
1523
|
+
"display_name": f"{model_dir_name} | Fine-tuned Diffuser"
|
|
1524
|
+
})
|
|
1525
|
+
continue # Move to the next model_dir_name found in this root_path
|
|
1526
|
+
|
|
1527
|
+
print(f"🌋 Skipping {full_model_path}: No model_final.pt or checkpoints directory found at root.")
|
|
1528
|
+
|
|
1529
|
+
print(f"🌋 Finished scanning. Found {len(finetuned_models)} fine-tuned models.")
|
|
1530
|
+
return jsonify({"models": finetuned_models, "error": None})
|
|
1531
|
+
|
|
1532
|
+
@app.route('/api/finetune_diffusers', methods=['POST'])
|
|
1533
|
+
def finetune_diffusers():
|
|
1534
|
+
data = request.json
|
|
1535
|
+
images = data.get('images', [])
|
|
1536
|
+
captions = data.get('captions', [])
|
|
1537
|
+
output_name = data.get('outputName', 'my_diffusion_model')
|
|
1538
|
+
num_epochs = data.get('epochs', 100)
|
|
1539
|
+
batch_size = data.get('batchSize', 4)
|
|
1540
|
+
learning_rate = data.get('learningRate', 1e-4)
|
|
1541
|
+
output_path = data.get('outputPath', '~/.npcsh/models')
|
|
1542
|
+
|
|
1543
|
+
print(f"🌋 Finetune Diffusers Request Received!")
|
|
1544
|
+
print(f" Images: {len(images)} files")
|
|
1545
|
+
print(f" Output Name: {output_name}")
|
|
1546
|
+
print(f" Epochs: {num_epochs}, Batch Size: {batch_size}, Learning Rate: {learning_rate}")
|
|
1547
|
+
|
|
1548
|
+
if not images:
|
|
1549
|
+
print("🌋 Error: No images provided for finetuning.")
|
|
1550
|
+
return jsonify({'error': 'No images provided'}), 400
|
|
1551
|
+
|
|
1552
|
+
if not captions or len(captions) != len(images):
|
|
1553
|
+
print("🌋 Warning: Captions not provided or mismatching image count. Using empty captions.")
|
|
1554
|
+
captions = [''] * len(images)
|
|
1555
|
+
|
|
1556
|
+
expanded_images = [os.path.expanduser(p) for p in images]
|
|
1557
|
+
output_dir = os.path.expanduser(
|
|
1558
|
+
os.path.join(output_path, output_name)
|
|
1559
|
+
)
|
|
1560
|
+
|
|
1561
|
+
job_id = f"ft_{int(time.time())}"
|
|
1562
|
+
finetune_jobs[job_id] = {
|
|
1563
|
+
'status': 'running',
|
|
1564
|
+
'output_dir': output_dir,
|
|
1565
|
+
'epochs': num_epochs,
|
|
1566
|
+
'current_epoch': 0,
|
|
1567
|
+
'start_time': datetime.datetime.now().isoformat()
|
|
1568
|
+
}
|
|
1569
|
+
print(f"🌋 Finetuning job {job_id} initialized. Output directory: {output_dir}")
|
|
1570
|
+
|
|
1571
|
+
def run_training_async():
|
|
1572
|
+
print(f"🌋 Finetuning job {job_id}: Starting asynchronous training thread...")
|
|
1573
|
+
try:
|
|
1574
|
+
config = DiffusionConfig(
|
|
1575
|
+
num_epochs=num_epochs,
|
|
1576
|
+
batch_size=batch_size,
|
|
1577
|
+
learning_rate=learning_rate,
|
|
1578
|
+
output_model_path=output_dir
|
|
1579
|
+
)
|
|
1580
|
+
|
|
1581
|
+
print(f"🌋 Finetuning job {job_id}: Calling train_diffusion with config: {config}")
|
|
1582
|
+
# Assuming train_diffusion might print its own progress or allow callbacks
|
|
1583
|
+
# For more granular logging, you'd need to modify train_diffusion itself
|
|
1584
|
+
model_path = train_diffusion(
|
|
1585
|
+
expanded_images,
|
|
1586
|
+
captions,
|
|
1587
|
+
config=config
|
|
1588
|
+
)
|
|
1589
|
+
|
|
1590
|
+
finetune_jobs[job_id]['status'] = 'complete'
|
|
1591
|
+
finetune_jobs[job_id]['model_path'] = model_path
|
|
1592
|
+
finetune_jobs[job_id]['end_time'] = datetime.datetime.now().isoformat()
|
|
1593
|
+
print(f"🌋 Finetuning job {job_id}: Training complete! Model saved to: {model_path}")
|
|
1594
|
+
except Exception as e:
|
|
1595
|
+
finetune_jobs[job_id]['status'] = 'error'
|
|
1596
|
+
finetune_jobs[job_id]['error_msg'] = str(e)
|
|
1597
|
+
finetune_jobs[job_id]['end_time'] = datetime.datetime.now().isoformat()
|
|
1598
|
+
print(f"🌋 Finetuning job {job_id}: ERROR during training: {e}")
|
|
1599
|
+
traceback.print_exc()
|
|
1600
|
+
print(f"🌋 Finetuning job {job_id}: Asynchronous training thread finished.")
|
|
1601
|
+
|
|
1602
|
+
# Start the training in a separate thread
|
|
1603
|
+
thread = threading.Thread(target=run_training_async)
|
|
1604
|
+
thread.daemon = True # Allow the main program to exit even if this thread is still running
|
|
1605
|
+
thread.start()
|
|
1606
|
+
|
|
1607
|
+
print(f"🌋 Finetuning job {job_id} successfully launched in background. Returning initial status.")
|
|
1608
|
+
return jsonify({
|
|
1609
|
+
'status': 'started',
|
|
1610
|
+
'jobId': job_id,
|
|
1611
|
+
'message': f"Finetuning job '{job_id}' started. Check /api/finetune_status/{job_id} for updates."
|
|
1612
|
+
})
|
|
1613
|
+
|
|
1614
|
+
|
|
1615
|
+
@app.route('/api/finetune_status/<job_id>', methods=['GET'])
|
|
1616
|
+
def finetune_status(job_id):
|
|
1617
|
+
if job_id not in finetune_jobs:
|
|
1618
|
+
return jsonify({'error': 'Job not found'}), 404
|
|
1619
|
+
|
|
1620
|
+
job = finetune_jobs[job_id]
|
|
1621
|
+
|
|
1622
|
+
if job['status'] == 'complete':
|
|
1623
|
+
return jsonify({
|
|
1624
|
+
'complete': True,
|
|
1625
|
+
'outputPath': job.get('model_path', job['output_dir'])
|
|
1626
|
+
})
|
|
1627
|
+
elif job['status'] == 'error':
|
|
1628
|
+
return jsonify({'error': job.get('error_msg', 'Unknown error')})
|
|
1629
|
+
|
|
1630
|
+
return jsonify({
|
|
1631
|
+
'step': job.get('current_epoch', 0),
|
|
1632
|
+
'total': job['epochs'],
|
|
1633
|
+
'status': 'running'
|
|
1634
|
+
})
|
|
1635
|
+
|
|
1636
|
+
@app.route("/api/ml/train", methods=["POST"])
|
|
1637
|
+
def train_ml_model():
|
|
1638
|
+
import pickle
|
|
1639
|
+
import numpy as np
|
|
1640
|
+
from sklearn.linear_model import LinearRegression, LogisticRegression
|
|
1641
|
+
from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor
|
|
1642
|
+
from sklearn.tree import DecisionTreeRegressor
|
|
1643
|
+
from sklearn.cluster import KMeans
|
|
1644
|
+
from sklearn.model_selection import train_test_split
|
|
1645
|
+
from sklearn.metrics import mean_squared_error, r2_score, accuracy_score
|
|
1646
|
+
|
|
1647
|
+
data = request.json
|
|
1648
|
+
model_name = data.get("name")
|
|
1649
|
+
model_type = data.get("type")
|
|
1650
|
+
target = data.get("target")
|
|
1651
|
+
features = data.get("features")
|
|
1652
|
+
training_data = data.get("data")
|
|
1653
|
+
hyperparams = data.get("hyperparameters", {})
|
|
1654
|
+
|
|
1655
|
+
df = pd.DataFrame(training_data)
|
|
1656
|
+
X = df[features].values
|
|
1657
|
+
|
|
1658
|
+
metrics = {}
|
|
1659
|
+
model = None
|
|
1660
|
+
|
|
1661
|
+
if model_type == "linear_regression":
|
|
1662
|
+
y = df[target].values
|
|
1663
|
+
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
|
|
1664
|
+
model = LinearRegression()
|
|
1665
|
+
model.fit(X_train, y_train)
|
|
1666
|
+
y_pred = model.predict(X_test)
|
|
1667
|
+
metrics = {
|
|
1668
|
+
"r2_score": r2_score(y_test, y_pred),
|
|
1669
|
+
"rmse": np.sqrt(mean_squared_error(y_test, y_pred))
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
elif model_type == "logistic_regression":
|
|
1673
|
+
y = df[target].values
|
|
1674
|
+
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
|
|
1675
|
+
model = LogisticRegression(max_iter=1000)
|
|
1676
|
+
model.fit(X_train, y_train)
|
|
1677
|
+
y_pred = model.predict(X_test)
|
|
1678
|
+
metrics = {"accuracy": accuracy_score(y_test, y_pred)}
|
|
1679
|
+
|
|
1680
|
+
elif model_type == "random_forest":
|
|
1681
|
+
y = df[target].values
|
|
1682
|
+
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
|
|
1683
|
+
model = RandomForestRegressor(n_estimators=100)
|
|
1684
|
+
model.fit(X_train, y_train)
|
|
1685
|
+
y_pred = model.predict(X_test)
|
|
1686
|
+
metrics = {
|
|
1687
|
+
"r2_score": r2_score(y_test, y_pred),
|
|
1688
|
+
"rmse": np.sqrt(mean_squared_error(y_test, y_pred))
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
elif model_type == "clustering":
|
|
1692
|
+
n_clusters = hyperparams.get("n_clusters", 3)
|
|
1693
|
+
model = KMeans(n_clusters=n_clusters)
|
|
1694
|
+
labels = model.fit_predict(X)
|
|
1695
|
+
metrics = {"inertia": model.inertia_, "n_clusters": n_clusters}
|
|
1696
|
+
|
|
1697
|
+
elif model_type == "gradient_boost":
|
|
1698
|
+
y = df[target].values
|
|
1699
|
+
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
|
|
1700
|
+
model = GradientBoostingRegressor()
|
|
1701
|
+
model.fit(X_train, y_train)
|
|
1702
|
+
y_pred = model.predict(X_test)
|
|
1703
|
+
metrics = {
|
|
1704
|
+
"r2_score": r2_score(y_test, y_pred),
|
|
1705
|
+
"rmse": np.sqrt(mean_squared_error(y_test, y_pred))
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
model_id = f"{model_name}_{int(time.time())}"
|
|
1709
|
+
model_path = os.path.expanduser(f"~/.npcsh/models/{model_id}.pkl")
|
|
1710
|
+
os.makedirs(os.path.dirname(model_path), exist_ok=True)
|
|
1711
|
+
|
|
1712
|
+
with open(model_path, 'wb') as f:
|
|
1713
|
+
pickle.dump({
|
|
1714
|
+
"model": model,
|
|
1715
|
+
"features": features,
|
|
1716
|
+
"target": target,
|
|
1717
|
+
"type": model_type
|
|
1718
|
+
}, f)
|
|
1719
|
+
|
|
1720
|
+
return jsonify({
|
|
1721
|
+
"model_id": model_id,
|
|
1722
|
+
"metrics": metrics,
|
|
1723
|
+
"error": None
|
|
1724
|
+
})
|
|
1053
1725
|
|
|
1054
1726
|
|
|
1727
|
+
@app.route("/api/ml/predict", methods=["POST"])
|
|
1728
|
+
def ml_predict():
|
|
1729
|
+
import pickle
|
|
1730
|
+
|
|
1731
|
+
data = request.json
|
|
1732
|
+
model_name = data.get("model_name")
|
|
1733
|
+
input_data = data.get("input_data")
|
|
1734
|
+
|
|
1735
|
+
model_dir = os.path.expanduser("~/.npcsh/models/")
|
|
1736
|
+
model_files = [f for f in os.listdir(model_dir) if f.startswith(model_name)]
|
|
1737
|
+
|
|
1738
|
+
if not model_files:
|
|
1739
|
+
return jsonify({"error": f"Model {model_name} not found"})
|
|
1740
|
+
|
|
1741
|
+
model_path = os.path.join(model_dir, model_files[0])
|
|
1742
|
+
|
|
1743
|
+
with open(model_path, 'rb') as f:
|
|
1744
|
+
model_data = pickle.load(f)
|
|
1745
|
+
|
|
1746
|
+
model = model_data["model"]
|
|
1747
|
+
prediction = model.predict([input_data])
|
|
1748
|
+
|
|
1749
|
+
return jsonify({
|
|
1750
|
+
"prediction": prediction.tolist(),
|
|
1751
|
+
"error": None
|
|
1752
|
+
})
|
|
1753
|
+
@app.route("/api/jinx/executions/label", methods=["POST"])
|
|
1754
|
+
def label_jinx_execution():
|
|
1755
|
+
data = request.json
|
|
1756
|
+
execution_id = data.get("executionId")
|
|
1757
|
+
label = data.get("label")
|
|
1758
|
+
|
|
1759
|
+
command_history = CommandHistory(app.config.get('DB_PATH'))
|
|
1760
|
+
command_history.label_jinx_execution(execution_id, label)
|
|
1761
|
+
|
|
1762
|
+
return jsonify({"success": True, "error": None})
|
|
1763
|
+
|
|
1764
|
+
|
|
1765
|
+
@app.route("/api/npc/executions", methods=["GET"])
|
|
1766
|
+
def get_npc_executions():
|
|
1767
|
+
npc_name = request.args.get("npcName")
|
|
1768
|
+
|
|
1769
|
+
|
|
1770
|
+
command_history = CommandHistory(app.config.get('DB_PATH'))
|
|
1771
|
+
executions = command_history.get_npc_executions(npc_name)
|
|
1772
|
+
|
|
1773
|
+
return jsonify({"executions": executions, "error": None})
|
|
1774
|
+
|
|
1775
|
+
|
|
1776
|
+
@app.route("/api/npc/executions/label", methods=["POST"])
|
|
1777
|
+
def label_npc_execution():
|
|
1778
|
+
data = request.json
|
|
1779
|
+
execution_id = data.get("executionId")
|
|
1780
|
+
label = data.get("label")
|
|
1781
|
+
|
|
1782
|
+
command_history = CommandHistory(app.config.get('DB_PATH'))
|
|
1783
|
+
command_history.label_npc_execution(execution_id, label)
|
|
1784
|
+
|
|
1785
|
+
return jsonify({"success": True, "error": None})
|
|
1786
|
+
|
|
1787
|
+
|
|
1788
|
+
@app.route("/api/training/dataset", methods=["POST"])
|
|
1789
|
+
def build_training_dataset():
|
|
1790
|
+
data = request.json
|
|
1791
|
+
filters = data.get("filters", {})
|
|
1792
|
+
|
|
1793
|
+
command_history = CommandHistory(app.config.get('DB_PATH'))
|
|
1794
|
+
dataset = command_history.get_training_dataset(
|
|
1795
|
+
include_jinxs=filters.get("jinxs", True),
|
|
1796
|
+
include_npcs=filters.get("npcs", True),
|
|
1797
|
+
npc_names=filters.get("npc_names")
|
|
1798
|
+
)
|
|
1799
|
+
|
|
1800
|
+
return jsonify({
|
|
1801
|
+
"dataset": dataset,
|
|
1802
|
+
"count": len(dataset),
|
|
1803
|
+
"error": None
|
|
1804
|
+
})
|
|
1055
1805
|
@app.route("/api/save_npc", methods=["POST"])
|
|
1056
1806
|
def save_npc():
|
|
1057
1807
|
try:
|
|
@@ -1092,137 +1842,147 @@ use_global_jinxs: {str(npc_data.get('use_global_jinxs', True)).lower()}
|
|
|
1092
1842
|
print(f"Error saving NPC: {str(e)}")
|
|
1093
1843
|
return jsonify({"error": str(e)}), 500
|
|
1094
1844
|
|
|
1095
|
-
@app.route("/api/
|
|
1096
|
-
def
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
global_npc_directory = os.path.expanduser("~/.npcsh/npc_team")
|
|
1100
|
-
|
|
1101
|
-
npc_data = []
|
|
1845
|
+
@app.route("/api/jinxs/global")
|
|
1846
|
+
def get_jinxs_global():
|
|
1847
|
+
global_jinx_directory = os.path.expanduser("~/.npcsh/npc_team/jinxs")
|
|
1848
|
+
jinx_data = []
|
|
1102
1849
|
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
print(f"Global NPC directory not found: {global_npc_directory}", file=sys.stderr)
|
|
1106
|
-
return jsonify({"npcs": [], "error": f"Global NPC directory not found: {global_npc_directory}"})
|
|
1850
|
+
if not os.path.exists(global_jinx_directory):
|
|
1851
|
+
return jsonify({"jinxs": [], "error": None})
|
|
1107
1852
|
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
"engine": step.get("engine", "natural"),
|
|
1135
|
-
"code": step.get("code", "")
|
|
1136
|
-
}
|
|
1137
|
-
for i, step in enumerate(jinx.steps)
|
|
1138
|
-
]
|
|
1139
|
-
}
|
|
1140
|
-
for jinx in npc.jinxs_dict.values() # Use jinxs_dict here
|
|
1141
|
-
] if hasattr(npc, 'jinxs_dict') else [], # Defensive check
|
|
1142
|
-
}
|
|
1143
|
-
npc_data.append(serialized_npc)
|
|
1144
|
-
except Exception as e:
|
|
1145
|
-
print(f"Error loading or serializing NPC {file}: {str(e)}", file=sys.stderr)
|
|
1146
|
-
traceback.print_exc(file=sys.stderr)
|
|
1853
|
+
for root, dirs, files in os.walk(global_jinx_directory):
|
|
1854
|
+
for file in files:
|
|
1855
|
+
if file.endswith(".jinx"):
|
|
1856
|
+
jinx_path = os.path.join(root, file)
|
|
1857
|
+
with open(jinx_path, 'r') as f:
|
|
1858
|
+
raw_data = yaml.safe_load(f)
|
|
1859
|
+
|
|
1860
|
+
inputs = []
|
|
1861
|
+
for inp in raw_data.get("inputs", []):
|
|
1862
|
+
if isinstance(inp, str):
|
|
1863
|
+
inputs.append(inp)
|
|
1864
|
+
elif isinstance(inp, dict):
|
|
1865
|
+
inputs.append(list(inp.keys())[0])
|
|
1866
|
+
else:
|
|
1867
|
+
inputs.append(str(inp))
|
|
1868
|
+
|
|
1869
|
+
rel_path = os.path.relpath(jinx_path, global_jinx_directory)
|
|
1870
|
+
path_without_ext = rel_path[:-5]
|
|
1871
|
+
|
|
1872
|
+
jinx_data.append({
|
|
1873
|
+
"jinx_name": raw_data.get("jinx_name", file[:-5]),
|
|
1874
|
+
"path": path_without_ext,
|
|
1875
|
+
"description": raw_data.get("description", ""),
|
|
1876
|
+
"inputs": inputs,
|
|
1877
|
+
"steps": raw_data.get("steps", [])
|
|
1878
|
+
})
|
|
1147
1879
|
|
|
1880
|
+
return jsonify({"jinxs": jinx_data, "error": None})
|
|
1148
1881
|
|
|
1149
|
-
|
|
1882
|
+
@app.route("/api/jinxs/project", methods=["GET"])
|
|
1883
|
+
def get_jinxs_project():
|
|
1884
|
+
project_dir = request.args.get("currentPath")
|
|
1885
|
+
if not project_dir:
|
|
1886
|
+
return jsonify({"jinxs": [], "error": "currentPath required"}), 400
|
|
1150
1887
|
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
traceback.print_exc(file=sys.stderr)
|
|
1154
|
-
return jsonify({"npcs": [], "error": str(e)})
|
|
1888
|
+
if not project_dir.endswith("jinxs"):
|
|
1889
|
+
project_dir = os.path.join(project_dir, "jinxs")
|
|
1155
1890
|
|
|
1891
|
+
jinx_data = []
|
|
1892
|
+
if not os.path.exists(project_dir):
|
|
1893
|
+
return jsonify({"jinxs": [], "error": None})
|
|
1156
1894
|
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1895
|
+
for root, dirs, files in os.walk(project_dir):
|
|
1896
|
+
for file in files:
|
|
1897
|
+
if file.endswith(".jinx"):
|
|
1898
|
+
jinx_path = os.path.join(root, file)
|
|
1899
|
+
with open(jinx_path, 'r') as f:
|
|
1900
|
+
raw_data = yaml.safe_load(f)
|
|
1901
|
+
|
|
1902
|
+
inputs = []
|
|
1903
|
+
for inp in raw_data.get("inputs", []):
|
|
1904
|
+
if isinstance(inp, str):
|
|
1905
|
+
inputs.append(inp)
|
|
1906
|
+
elif isinstance(inp, dict):
|
|
1907
|
+
inputs.append(list(inp.keys())[0])
|
|
1908
|
+
else:
|
|
1909
|
+
inputs.append(str(inp))
|
|
1910
|
+
|
|
1911
|
+
rel_path = os.path.relpath(jinx_path, project_dir)
|
|
1912
|
+
path_without_ext = rel_path[:-5]
|
|
1913
|
+
|
|
1914
|
+
jinx_data.append({
|
|
1915
|
+
"jinx_name": raw_data.get("jinx_name", file[:-5]),
|
|
1916
|
+
"path": path_without_ext,
|
|
1917
|
+
"description": raw_data.get("description", ""),
|
|
1918
|
+
"inputs": inputs,
|
|
1919
|
+
"steps": raw_data.get("steps", [])
|
|
1920
|
+
})
|
|
1921
|
+
print(jinx_data)
|
|
1922
|
+
return jsonify({"jinxs": jinx_data, "error": None})
|
|
1161
1923
|
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1924
|
+
@app.route("/api/npc_team_global")
|
|
1925
|
+
def get_npc_team_global():
|
|
1926
|
+
global_npc_directory = os.path.expanduser("~/.npcsh/npc_team")
|
|
1927
|
+
npc_data = []
|
|
1165
1928
|
|
|
1166
|
-
|
|
1167
|
-
|
|
1929
|
+
if not os.path.exists(global_npc_directory):
|
|
1930
|
+
return jsonify({"npcs": [], "error": None})
|
|
1168
1931
|
|
|
1169
|
-
|
|
1932
|
+
for file in os.listdir(global_npc_directory):
|
|
1933
|
+
if file.endswith(".npc"):
|
|
1934
|
+
npc_path = os.path.join(global_npc_directory, file)
|
|
1935
|
+
with open(npc_path, 'r') as f:
|
|
1936
|
+
raw_data = yaml.safe_load(f)
|
|
1937
|
+
|
|
1938
|
+
npc_data.append({
|
|
1939
|
+
"name": raw_data.get("name", file[:-4]),
|
|
1940
|
+
"primary_directive": raw_data.get("primary_directive", ""),
|
|
1941
|
+
"model": raw_data.get("model", ""),
|
|
1942
|
+
"provider": raw_data.get("provider", ""),
|
|
1943
|
+
"api_url": raw_data.get("api_url", ""),
|
|
1944
|
+
"use_global_jinxs": raw_data.get("use_global_jinxs", True),
|
|
1945
|
+
"jinxs": raw_data.get("jinxs", "*"),
|
|
1946
|
+
})
|
|
1170
1947
|
|
|
1171
|
-
|
|
1172
|
-
if not os.path.exists(project_npc_directory):
|
|
1173
|
-
print(f"Project NPC directory not found: {project_npc_directory}", file=sys.stderr)
|
|
1174
|
-
return jsonify({"npcs": [], "error": f"Project NPC directory not found: {project_npc_directory}"})
|
|
1948
|
+
return jsonify({"npcs": npc_data, "error": None})
|
|
1175
1949
|
|
|
1176
|
-
for file in os.listdir(project_npc_directory):
|
|
1177
|
-
print(f"Processing project NPC file: {file}", file=sys.stderr) # Diagnostic print
|
|
1178
|
-
if file.endswith(".npc"):
|
|
1179
|
-
npc_path = os.path.join(project_npc_directory, file)
|
|
1180
|
-
try:
|
|
1181
|
-
npc = NPC(file=npc_path, db_conn=db_conn)
|
|
1182
|
-
|
|
1183
|
-
# Ensure jinxs are initialized after NPC creation if not already
|
|
1184
|
-
# This is crucial for populating npc.jinxs_dict
|
|
1185
|
-
if not npc.jinxs_dict and hasattr(npc, 'initialize_jinxs'):
|
|
1186
|
-
npc.initialize_jinxs()
|
|
1187
|
-
|
|
1188
|
-
serialized_npc = {
|
|
1189
|
-
"name": npc.name,
|
|
1190
|
-
"primary_directive": npc.primary_directive,
|
|
1191
|
-
"model": npc.model,
|
|
1192
|
-
"provider": npc.provider,
|
|
1193
|
-
"api_url": npc.api_url,
|
|
1194
|
-
"use_global_jinxs": npc.use_global_jinxs,
|
|
1195
|
-
# CRITICAL FIX: Iterate over npc.jinxs_dict.values() which contains Jinx objects
|
|
1196
|
-
"jinxs": [
|
|
1197
|
-
{
|
|
1198
|
-
"jinx_name": jinx.jinx_name,
|
|
1199
|
-
"inputs": jinx.inputs,
|
|
1200
|
-
"steps": [
|
|
1201
|
-
{
|
|
1202
|
-
"name": step.get("name", f"step_{i}"),
|
|
1203
|
-
"engine": step.get("engine", "natural"),
|
|
1204
|
-
"code": step.get("code", "")
|
|
1205
|
-
}
|
|
1206
|
-
for i, step in enumerate(jinx.steps)
|
|
1207
|
-
]
|
|
1208
|
-
}
|
|
1209
|
-
for jinx in npc.jinxs_dict.values() # Use jinxs_dict here
|
|
1210
|
-
] if hasattr(npc, 'jinxs_dict') else [], # Defensive check
|
|
1211
|
-
}
|
|
1212
|
-
npc_data.append(serialized_npc)
|
|
1213
|
-
except Exception as e:
|
|
1214
|
-
print(f"Error loading or serializing NPC {file}: {str(e)}", file=sys.stderr)
|
|
1215
|
-
traceback.print_exc(file=sys.stderr)
|
|
1216
1950
|
|
|
1951
|
+
@app.route("/api/npc_team_project", methods=["GET"])
|
|
1952
|
+
def get_npc_team_project():
|
|
1953
|
+
project_npc_directory = request.args.get("currentPath")
|
|
1954
|
+
if not project_npc_directory:
|
|
1955
|
+
return jsonify({"npcs": [], "error": "currentPath required"}), 400
|
|
1956
|
+
|
|
1957
|
+
if not project_npc_directory.endswith("npc_team"):
|
|
1958
|
+
project_npc_directory = os.path.join(
|
|
1959
|
+
project_npc_directory,
|
|
1960
|
+
"npc_team"
|
|
1961
|
+
)
|
|
1217
1962
|
|
|
1218
|
-
|
|
1219
|
-
return jsonify({"npcs": npc_data, "error": None})
|
|
1963
|
+
npc_data = []
|
|
1220
1964
|
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
traceback.print_exc(file=sys.stderr)
|
|
1224
|
-
return jsonify({"npcs": [], "error": str(e)})
|
|
1965
|
+
if not os.path.exists(project_npc_directory):
|
|
1966
|
+
return jsonify({"npcs": [], "error": None})
|
|
1225
1967
|
|
|
1968
|
+
for file in os.listdir(project_npc_directory):
|
|
1969
|
+
if file.endswith(".npc"):
|
|
1970
|
+
npc_path = os.path.join(project_npc_directory, file)
|
|
1971
|
+
with open(npc_path, 'r') as f:
|
|
1972
|
+
raw_npc_data = yaml.safe_load(f)
|
|
1973
|
+
|
|
1974
|
+
serialized_npc = {
|
|
1975
|
+
"name": raw_npc_data.get("name", file[:-4]),
|
|
1976
|
+
"primary_directive": raw_npc_data.get("primary_directive", ""),
|
|
1977
|
+
"model": raw_npc_data.get("model", ""),
|
|
1978
|
+
"provider": raw_npc_data.get("provider", ""),
|
|
1979
|
+
"api_url": raw_npc_data.get("api_url", ""),
|
|
1980
|
+
"use_global_jinxs": raw_npc_data.get("use_global_jinxs", True),
|
|
1981
|
+
"jinxs": raw_npc_data.get("jinxs", "*"),
|
|
1982
|
+
}
|
|
1983
|
+
npc_data.append(serialized_npc)
|
|
1984
|
+
|
|
1985
|
+
return jsonify({"npcs": npc_data, "error": None})
|
|
1226
1986
|
|
|
1227
1987
|
def get_last_used_model_and_npc_in_directory(directory_path):
|
|
1228
1988
|
"""
|
|
@@ -1542,11 +2302,62 @@ IMAGE_MODELS = {
|
|
|
1542
2302
|
{"value": "runwayml/stable-diffusion-v1-5", "display_name": "Stable Diffusion v1.5"},
|
|
1543
2303
|
],
|
|
1544
2304
|
}
|
|
2305
|
+
# In npcpy/serve.py, find the @app.route('/api/finetuned_models', methods=['GET'])
|
|
2306
|
+
# and replace the entire function with this:
|
|
1545
2307
|
|
|
2308
|
+
# This is now an internal helper function, not a Flask route.
|
|
2309
|
+
def _get_finetuned_models_internal(current_path=None): # Renamed to indicate internal use
|
|
2310
|
+
|
|
2311
|
+
# Define a list of potential root directories where fine-tuned models might be saved.
|
|
2312
|
+
potential_root_paths = [
|
|
2313
|
+
os.path.expanduser('~/.npcsh/models'), # Standard global models directory
|
|
2314
|
+
os.path.expanduser('~/.npcsh/images'), # Global images directory (where user's model was saved)
|
|
2315
|
+
]
|
|
2316
|
+
if current_path:
|
|
2317
|
+
# Add project-specific model directories if a current_path is provided
|
|
2318
|
+
project_models_path = os.path.join(current_path, 'models')
|
|
2319
|
+
project_images_path = os.path.join(current_path, 'images') # Also check project images directory
|
|
2320
|
+
potential_root_paths.extend([project_models_path, project_images_path])
|
|
2321
|
+
|
|
2322
|
+
finetuned_models = []
|
|
2323
|
+
|
|
2324
|
+
print(f"🌋 (Internal) Searching for fine-tuned models in potential root paths: {set(potential_root_paths)}")
|
|
2325
|
+
|
|
2326
|
+
for root_path in set(potential_root_paths):
|
|
2327
|
+
if not os.path.exists(root_path) or not os.path.isdir(root_path):
|
|
2328
|
+
print(f"🌋 (Internal) Skipping non-existent or non-directory root path: {root_path}")
|
|
2329
|
+
continue
|
|
2330
|
+
|
|
2331
|
+
print(f"🌋 (Internal) Scanning root path: {root_path}")
|
|
2332
|
+
for model_dir_name in os.listdir(root_path):
|
|
2333
|
+
full_model_path = os.path.join(root_path, model_dir_name)
|
|
2334
|
+
|
|
2335
|
+
if not os.path.isdir(full_model_path):
|
|
2336
|
+
print(f"🌋 (Internal) Skipping {full_model_path}: Not a directory.")
|
|
2337
|
+
continue
|
|
2338
|
+
|
|
2339
|
+
# Check for 'model_final.pt' or the 'checkpoints' directory
|
|
2340
|
+
has_model_final_pt = os.path.exists(os.path.join(full_model_path, 'model_final.pt'))
|
|
2341
|
+
has_checkpoints_dir = os.path.isdir(os.path.join(full_model_path, 'checkpoints'))
|
|
2342
|
+
|
|
2343
|
+
if has_model_final_pt or has_checkpoints_dir:
|
|
2344
|
+
print(f"🌋 (Internal) Identified fine-tuned model: {model_dir_name} at {full_model_path} (found model_final.pt or checkpoints dir)")
|
|
2345
|
+
finetuned_models.append({
|
|
2346
|
+
"value": full_model_path, # This is the path to the directory containing the .pt files
|
|
2347
|
+
"provider": "diffusers", # Provider is still "diffusers"
|
|
2348
|
+
"display_name": f"{model_dir_name} | Fine-tuned Diffuser"
|
|
2349
|
+
})
|
|
2350
|
+
continue
|
|
2351
|
+
|
|
2352
|
+
print(f"🌋 (Internal) Skipping {full_model_path}: No model_final.pt or checkpoints directory found at root.")
|
|
2353
|
+
|
|
2354
|
+
print(f"🌋 (Internal) Finished scanning. Found {len(finetuned_models)} fine-tuned models.")
|
|
2355
|
+
# <--- CRITICAL FIX: Directly return the list of models, not a Flask Response
|
|
2356
|
+
return {"models": finetuned_models, "error": None} # Return a dict for consistency
|
|
1546
2357
|
def get_available_image_models(current_path=None):
|
|
1547
2358
|
"""
|
|
1548
2359
|
Retrieves available image generation models based on environment variables
|
|
1549
|
-
and predefined configurations.
|
|
2360
|
+
and predefined configurations, including locally fine-tuned Diffusers models.
|
|
1550
2361
|
"""
|
|
1551
2362
|
|
|
1552
2363
|
if current_path:
|
|
@@ -1554,7 +2365,7 @@ def get_available_image_models(current_path=None):
|
|
|
1554
2365
|
|
|
1555
2366
|
all_image_models = []
|
|
1556
2367
|
|
|
1557
|
-
|
|
2368
|
+
# Add models configured via environment variables
|
|
1558
2369
|
env_image_model = os.getenv("NPCSH_IMAGE_MODEL")
|
|
1559
2370
|
env_image_provider = os.getenv("NPCSH_IMAGE_PROVIDER")
|
|
1560
2371
|
|
|
@@ -1565,9 +2376,8 @@ def get_available_image_models(current_path=None):
|
|
|
1565
2376
|
"display_name": f"{env_image_model} | {env_image_provider} (Configured)"
|
|
1566
2377
|
})
|
|
1567
2378
|
|
|
1568
|
-
|
|
2379
|
+
# Add predefined models (OpenAI, Gemini, and standard Diffusers)
|
|
1569
2380
|
for provider_key, models_list in IMAGE_MODELS.items():
|
|
1570
|
-
|
|
1571
2381
|
if provider_key == "openai":
|
|
1572
2382
|
if os.environ.get("OPENAI_API_KEY"):
|
|
1573
2383
|
all_image_models.extend([
|
|
@@ -1580,16 +2390,25 @@ def get_available_image_models(current_path=None):
|
|
|
1580
2390
|
{**model, "provider": provider_key, "display_name": f"{model['display_name']} | {provider_key}"}
|
|
1581
2391
|
for model in models_list
|
|
1582
2392
|
])
|
|
1583
|
-
elif provider_key == "diffusers":
|
|
1584
|
-
|
|
1585
|
-
|
|
2393
|
+
elif provider_key == "diffusers": # This entry in IMAGE_MODELS is for standard diffusers
|
|
1586
2394
|
all_image_models.extend([
|
|
1587
2395
|
{**model, "provider": provider_key, "display_name": f"{model['display_name']} | {provider_key}"}
|
|
1588
2396
|
for model in models_list
|
|
1589
2397
|
])
|
|
1590
2398
|
|
|
2399
|
+
# <--- CRITICAL FIX: Directly call the internal helper function for fine-tuned models
|
|
2400
|
+
try:
|
|
2401
|
+
finetuned_data_result = _get_finetuned_models_internal(current_path)
|
|
2402
|
+
if finetuned_data_result and finetuned_data_result.get("models"):
|
|
2403
|
+
all_image_models.extend(finetuned_data_result["models"])
|
|
2404
|
+
else:
|
|
2405
|
+
print(f"No fine-tuned models returned by internal helper or an error occurred internally.")
|
|
2406
|
+
if finetuned_data_result.get("error"):
|
|
2407
|
+
print(f"Internal error in _get_finetuned_models_internal: {finetuned_data_result['error']}")
|
|
2408
|
+
except Exception as e:
|
|
2409
|
+
print(f"Error calling _get_finetuned_models_internal: {e}")
|
|
1591
2410
|
|
|
1592
|
-
|
|
2411
|
+
# Deduplicate models
|
|
1593
2412
|
seen_models = set()
|
|
1594
2413
|
unique_models = []
|
|
1595
2414
|
for model_entry in all_image_models:
|
|
@@ -1598,6 +2417,7 @@ def get_available_image_models(current_path=None):
|
|
|
1598
2417
|
seen_models.add(key)
|
|
1599
2418
|
unique_models.append(model_entry)
|
|
1600
2419
|
|
|
2420
|
+
# Return the combined, deduplicated list of models as a dictionary with a 'models' key
|
|
1601
2421
|
return unique_models
|
|
1602
2422
|
|
|
1603
2423
|
@app.route('/api/generative_fill', methods=['POST'])
|
|
@@ -1824,11 +2644,13 @@ def generate_images():
|
|
|
1824
2644
|
if os.path.exists(image_path):
|
|
1825
2645
|
try:
|
|
1826
2646
|
pil_img = Image.open(image_path)
|
|
2647
|
+
pil_img = pil_img.convert("RGB")
|
|
2648
|
+
pil_img.thumbnail((1024, 1024))
|
|
1827
2649
|
input_images.append(pil_img)
|
|
1828
2650
|
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
2651
|
+
compressed_bytes = BytesIO()
|
|
2652
|
+
pil_img.save(compressed_bytes, format="JPEG", quality=85, optimize=True)
|
|
2653
|
+
img_data = compressed_bytes.getvalue()
|
|
1832
2654
|
attachments_loaded.append({
|
|
1833
2655
|
"name": os.path.basename(image_path),
|
|
1834
2656
|
"type": "images",
|
|
@@ -1932,20 +2754,31 @@ def get_mcp_tools():
|
|
|
1932
2754
|
It will try to use an existing client from corca_states if available and matching,
|
|
1933
2755
|
otherwise it creates a temporary client.
|
|
1934
2756
|
"""
|
|
1935
|
-
|
|
2757
|
+
raw_server_path = request.args.get("mcpServerPath")
|
|
2758
|
+
current_path_arg = request.args.get("currentPath")
|
|
1936
2759
|
conversation_id = request.args.get("conversationId")
|
|
1937
2760
|
npc_name = request.args.get("npc")
|
|
2761
|
+
selected_filter = request.args.get("selected", "")
|
|
2762
|
+
selected_names = [s.strip() for s in selected_filter.split(",") if s.strip()]
|
|
1938
2763
|
|
|
1939
|
-
if not
|
|
2764
|
+
if not raw_server_path:
|
|
1940
2765
|
return jsonify({"error": "mcpServerPath parameter is required."}), 400
|
|
1941
2766
|
|
|
1942
|
-
|
|
2767
|
+
# Normalize/expand the provided path so cwd/tilde don't break imports
|
|
2768
|
+
resolved_path = resolve_mcp_server_path(
|
|
2769
|
+
current_path=current_path_arg,
|
|
2770
|
+
explicit_path=raw_server_path,
|
|
2771
|
+
force_global=False
|
|
2772
|
+
)
|
|
2773
|
+
server_path = os.path.abspath(os.path.expanduser(resolved_path))
|
|
2774
|
+
|
|
1943
2775
|
try:
|
|
1944
2776
|
from npcsh.corca import MCPClientNPC
|
|
1945
2777
|
except ImportError:
|
|
1946
2778
|
return jsonify({"error": "MCP Client (npcsh.corca) not available. Ensure npcsh.corca is installed and importable."}), 500
|
|
1947
2779
|
|
|
1948
2780
|
temp_mcp_client = None
|
|
2781
|
+
jinx_tools = []
|
|
1949
2782
|
try:
|
|
1950
2783
|
|
|
1951
2784
|
if conversation_id and npc_name and hasattr(app, 'corca_states'):
|
|
@@ -1956,13 +2789,38 @@ def get_mcp_tools():
|
|
|
1956
2789
|
and existing_corca_state.mcp_client.server_script_path == server_path:
|
|
1957
2790
|
print(f"Using existing MCP client for {state_key} to fetch tools.")
|
|
1958
2791
|
temp_mcp_client = existing_corca_state.mcp_client
|
|
1959
|
-
|
|
2792
|
+
tools = temp_mcp_client.available_tools_llm
|
|
2793
|
+
if selected_names:
|
|
2794
|
+
tools = [t for t in tools if t.get("function", {}).get("name") in selected_names]
|
|
2795
|
+
return jsonify({"tools": tools, "error": None})
|
|
1960
2796
|
|
|
1961
2797
|
|
|
1962
2798
|
print(f"Creating a temporary MCP client to fetch tools for {server_path}.")
|
|
1963
2799
|
temp_mcp_client = MCPClientNPC()
|
|
1964
2800
|
if temp_mcp_client.connect_sync(server_path):
|
|
1965
|
-
|
|
2801
|
+
tools = temp_mcp_client.available_tools_llm
|
|
2802
|
+
# Append Jinx-derived tools discovered from global/project jinxs
|
|
2803
|
+
try:
|
|
2804
|
+
jinx_dirs = []
|
|
2805
|
+
if current_path_arg:
|
|
2806
|
+
proj_jinx_dir = os.path.join(os.path.abspath(current_path_arg), "npc_team", "jinxs")
|
|
2807
|
+
if os.path.isdir(proj_jinx_dir):
|
|
2808
|
+
jinx_dirs.append(proj_jinx_dir)
|
|
2809
|
+
global_jinx_dir = os.path.expanduser("~/.npcsh/npc_team/jinxs")
|
|
2810
|
+
if os.path.isdir(global_jinx_dir):
|
|
2811
|
+
jinx_dirs.append(global_jinx_dir)
|
|
2812
|
+
all_jinxs = []
|
|
2813
|
+
for d in jinx_dirs:
|
|
2814
|
+
all_jinxs.extend(load_jinxs_from_directory(d))
|
|
2815
|
+
if all_jinxs:
|
|
2816
|
+
jinx_tools = list(build_jinx_tool_catalog({j.jinx_name: j for j in all_jinxs}).values())
|
|
2817
|
+
print(f"[MCP] Discovered {len(jinx_tools)} Jinx tools for listing.")
|
|
2818
|
+
tools = tools + jinx_tools
|
|
2819
|
+
except Exception as e:
|
|
2820
|
+
print(f"[MCP] Error discovering Jinx tools for listing: {e}")
|
|
2821
|
+
if selected_names:
|
|
2822
|
+
tools = [t for t in tools if t.get("function", {}).get("name") in selected_names]
|
|
2823
|
+
return jsonify({"tools": tools, "error": None})
|
|
1966
2824
|
else:
|
|
1967
2825
|
return jsonify({"error": f"Failed to connect to MCP server at {server_path}."}), 500
|
|
1968
2826
|
except FileNotFoundError as e:
|
|
@@ -1981,6 +2839,64 @@ def get_mcp_tools():
|
|
|
1981
2839
|
temp_mcp_client.disconnect_sync()
|
|
1982
2840
|
|
|
1983
2841
|
|
|
2842
|
+
@app.route("/api/mcp/server/resolve", methods=["GET"])
|
|
2843
|
+
def api_mcp_resolve():
|
|
2844
|
+
current_path = request.args.get("currentPath")
|
|
2845
|
+
explicit = request.args.get("serverPath")
|
|
2846
|
+
try:
|
|
2847
|
+
resolved = resolve_mcp_server_path(current_path=current_path, explicit_path=explicit)
|
|
2848
|
+
return jsonify({"serverPath": resolved, "error": None})
|
|
2849
|
+
except Exception as e:
|
|
2850
|
+
return jsonify({"serverPath": None, "error": str(e)}), 500
|
|
2851
|
+
|
|
2852
|
+
|
|
2853
|
+
@app.route("/api/mcp/server/start", methods=["POST"])
|
|
2854
|
+
def api_mcp_start():
|
|
2855
|
+
data = request.get_json() or {}
|
|
2856
|
+
current_path = data.get("currentPath")
|
|
2857
|
+
explicit = data.get("serverPath")
|
|
2858
|
+
try:
|
|
2859
|
+
server_path = resolve_mcp_server_path(current_path=current_path, explicit_path=explicit)
|
|
2860
|
+
result = mcp_server_manager.start(server_path)
|
|
2861
|
+
return jsonify({**result, "error": None})
|
|
2862
|
+
except Exception as e:
|
|
2863
|
+
print(f"Error starting MCP server: {e}")
|
|
2864
|
+
traceback.print_exc()
|
|
2865
|
+
return jsonify({"error": str(e)}), 500
|
|
2866
|
+
|
|
2867
|
+
|
|
2868
|
+
@app.route("/api/mcp/server/stop", methods=["POST"])
|
|
2869
|
+
def api_mcp_stop():
|
|
2870
|
+
data = request.get_json() or {}
|
|
2871
|
+
explicit = data.get("serverPath")
|
|
2872
|
+
if not explicit:
|
|
2873
|
+
return jsonify({"error": "serverPath is required to stop a server."}), 400
|
|
2874
|
+
try:
|
|
2875
|
+
result = mcp_server_manager.stop(explicit)
|
|
2876
|
+
return jsonify({**result, "error": None})
|
|
2877
|
+
except Exception as e:
|
|
2878
|
+
print(f"Error stopping MCP server: {e}")
|
|
2879
|
+
traceback.print_exc()
|
|
2880
|
+
return jsonify({"error": str(e)}), 500
|
|
2881
|
+
|
|
2882
|
+
|
|
2883
|
+
@app.route("/api/mcp/server/status", methods=["GET"])
|
|
2884
|
+
def api_mcp_status():
|
|
2885
|
+
explicit = request.args.get("serverPath")
|
|
2886
|
+
current_path = request.args.get("currentPath")
|
|
2887
|
+
try:
|
|
2888
|
+
if explicit:
|
|
2889
|
+
result = mcp_server_manager.status(explicit)
|
|
2890
|
+
else:
|
|
2891
|
+
resolved = resolve_mcp_server_path(current_path=current_path, explicit_path=explicit)
|
|
2892
|
+
result = mcp_server_manager.status(resolved)
|
|
2893
|
+
return jsonify({**result, "running": result.get("status") == "running", "all": mcp_server_manager.running(), "error": None})
|
|
2894
|
+
except Exception as e:
|
|
2895
|
+
print(f"Error checking MCP server status: {e}")
|
|
2896
|
+
traceback.print_exc()
|
|
2897
|
+
return jsonify({"error": str(e)}), 500
|
|
2898
|
+
|
|
2899
|
+
|
|
1984
2900
|
@app.route("/api/image_models", methods=["GET"])
|
|
1985
2901
|
def get_image_models_api():
|
|
1986
2902
|
"""
|
|
@@ -1989,6 +2905,7 @@ def get_image_models_api():
|
|
|
1989
2905
|
current_path = request.args.get("currentPath")
|
|
1990
2906
|
try:
|
|
1991
2907
|
image_models = get_available_image_models(current_path)
|
|
2908
|
+
print('image models', image_models)
|
|
1992
2909
|
return jsonify({"models": image_models, "error": None})
|
|
1993
2910
|
except Exception as e:
|
|
1994
2911
|
print(f"Error getting available image models: {str(e)}")
|
|
@@ -2000,6 +2917,195 @@ def get_image_models_api():
|
|
|
2000
2917
|
|
|
2001
2918
|
|
|
2002
2919
|
|
|
2920
|
+
def _run_stream_post_processing(
|
|
2921
|
+
conversation_turn_text,
|
|
2922
|
+
conversation_id,
|
|
2923
|
+
command_history,
|
|
2924
|
+
npc_name,
|
|
2925
|
+
team_name,
|
|
2926
|
+
current_path,
|
|
2927
|
+
model,
|
|
2928
|
+
provider,
|
|
2929
|
+
npc_object,
|
|
2930
|
+
messages # For context compression
|
|
2931
|
+
):
|
|
2932
|
+
"""
|
|
2933
|
+
Runs memory extraction and context compression in a background thread.
|
|
2934
|
+
These operations will not block the main stream.
|
|
2935
|
+
"""
|
|
2936
|
+
print(f"🌋 Background task started for conversation {conversation_id}!")
|
|
2937
|
+
|
|
2938
|
+
# Memory extraction and KG fact insertion
|
|
2939
|
+
try:
|
|
2940
|
+
if len(conversation_turn_text) > 50: # Only extract memories if the turn is substantial
|
|
2941
|
+
memories_for_approval = extract_and_store_memories(
|
|
2942
|
+
conversation_turn_text,
|
|
2943
|
+
conversation_id,
|
|
2944
|
+
command_history,
|
|
2945
|
+
npc_name,
|
|
2946
|
+
team_name,
|
|
2947
|
+
current_path,
|
|
2948
|
+
model,
|
|
2949
|
+
provider,
|
|
2950
|
+
npc_object
|
|
2951
|
+
)
|
|
2952
|
+
if memories_for_approval:
|
|
2953
|
+
print(f"🔥 Background: Extracted {len(memories_for_approval)} memories for approval for conversation {conversation_id}. Stored as pending in the database (table: memory_lifecycle).")
|
|
2954
|
+
else:
|
|
2955
|
+
print(f"Background: Conversation turn too short ({len(conversation_turn_text)} chars) for memory extraction. Skipping.")
|
|
2956
|
+
except Exception as e:
|
|
2957
|
+
print(f"🌋 Background: Error during memory extraction and KG insertion for conversation {conversation_id}: {e}")
|
|
2958
|
+
traceback.print_exc()
|
|
2959
|
+
|
|
2960
|
+
# Context compression using breathe from llm_funcs
|
|
2961
|
+
try:
|
|
2962
|
+
if len(messages) > 30: # Use the threshold specified in your request
|
|
2963
|
+
# Directly call breathe for summarization
|
|
2964
|
+
breathe_result = breathe(
|
|
2965
|
+
messages=messages,
|
|
2966
|
+
model=model,
|
|
2967
|
+
provider=provider,
|
|
2968
|
+
npc=npc_object # Pass npc for context if available
|
|
2969
|
+
)
|
|
2970
|
+
compressed_output = breathe_result.get('output', '')
|
|
2971
|
+
|
|
2972
|
+
if compressed_output:
|
|
2973
|
+
# Save the compressed context as a new system message in conversation_history
|
|
2974
|
+
compressed_message_id = generate_message_id()
|
|
2975
|
+
save_conversation_message(
|
|
2976
|
+
command_history,
|
|
2977
|
+
conversation_id,
|
|
2978
|
+
"system", # Role for compressed context
|
|
2979
|
+
f"[AUTOMATIC CONTEXT COMPRESSION]: {compressed_output}",
|
|
2980
|
+
wd=current_path,
|
|
2981
|
+
model=model, # Use the same model/provider that generated the summary
|
|
2982
|
+
provider=provider,
|
|
2983
|
+
npc=npc_name, # Associate with the NPC
|
|
2984
|
+
team=team_name, # Associate with the team
|
|
2985
|
+
message_id=compressed_message_id
|
|
2986
|
+
)
|
|
2987
|
+
print(f"💨 Background: Compressed context for conversation {conversation_id} saved as new system message: {compressed_output[:100]}...")
|
|
2988
|
+
else:
|
|
2989
|
+
print(f"Background: Context compression returned no output for conversation {conversation_id}. Skipping saving.")
|
|
2990
|
+
else:
|
|
2991
|
+
print(f"Background: Conversation messages count ({len(messages)}) below threshold for context compression. Skipping.")
|
|
2992
|
+
except Exception as e:
|
|
2993
|
+
print(f"🌋 Background: Error during context compression with breathe for conversation {conversation_id}: {e}")
|
|
2994
|
+
traceback.print_exc()
|
|
2995
|
+
|
|
2996
|
+
print(f"🌋 Background task finished for conversation {conversation_id}!")
|
|
2997
|
+
|
|
2998
|
+
|
|
2999
|
+
|
|
3000
|
+
|
|
3001
|
+
@app.route("/api/text_predict", methods=["POST"])
|
|
3002
|
+
def text_predict():
|
|
3003
|
+
data = request.json
|
|
3004
|
+
|
|
3005
|
+
stream_id = data.get("streamId")
|
|
3006
|
+
if not stream_id:
|
|
3007
|
+
stream_id = str(uuid.uuid4())
|
|
3008
|
+
|
|
3009
|
+
with cancellation_lock:
|
|
3010
|
+
cancellation_flags[stream_id] = False
|
|
3011
|
+
|
|
3012
|
+
print(f"Starting text prediction stream with ID: {stream_id}")
|
|
3013
|
+
print('data')
|
|
3014
|
+
|
|
3015
|
+
|
|
3016
|
+
text_content = data.get("text_content", "")
|
|
3017
|
+
cursor_position = data.get("cursor_position", len(text_content))
|
|
3018
|
+
current_path = data.get("currentPath")
|
|
3019
|
+
model = data.get("model")
|
|
3020
|
+
provider = data.get("provider")
|
|
3021
|
+
context_type = data.get("context_type", "general") # e.g., 'code', 'chat', 'general'
|
|
3022
|
+
file_path = data.get("file_path") # Optional: for code context
|
|
3023
|
+
|
|
3024
|
+
if current_path:
|
|
3025
|
+
load_project_env(current_path)
|
|
3026
|
+
|
|
3027
|
+
text_before_cursor = text_content[:cursor_position]
|
|
3028
|
+
|
|
3029
|
+
|
|
3030
|
+
if context_type == 'code':
|
|
3031
|
+
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"
|
|
3032
|
+
system_prompt = "You are an AI code completion assistant. Only provide code. Do not add explanations or any other text."
|
|
3033
|
+
elif context_type == 'chat':
|
|
3034
|
+
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"
|
|
3035
|
+
system_prompt = "You are an AI chat assistant. Only provide natural language completion. Do not add explanations or any other text."
|
|
3036
|
+
else: # general text prediction
|
|
3037
|
+
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"
|
|
3038
|
+
system_prompt = "You are an AI text completion assistant. Only provide natural language completion. Do not add explanations or any other text."
|
|
3039
|
+
|
|
3040
|
+
|
|
3041
|
+
npc_object = None # For prediction, we don't necessarily use a specific NPC
|
|
3042
|
+
|
|
3043
|
+
messages_for_llm = [
|
|
3044
|
+
{"role": "system", "content": system_prompt},
|
|
3045
|
+
{"role": "user", "content": prompt_for_llm}
|
|
3046
|
+
]
|
|
3047
|
+
|
|
3048
|
+
def event_stream_text_predict(current_stream_id):
|
|
3049
|
+
complete_prediction = []
|
|
3050
|
+
try:
|
|
3051
|
+
stream_response_generator = get_llm_response(
|
|
3052
|
+
prompt_for_llm,
|
|
3053
|
+
messages=messages_for_llm,
|
|
3054
|
+
model=model,
|
|
3055
|
+
provider=provider,
|
|
3056
|
+
npc=npc_object,
|
|
3057
|
+
stream=True,
|
|
3058
|
+
)
|
|
3059
|
+
|
|
3060
|
+
# get_llm_response returns a dict with 'response' as a generator when stream=True
|
|
3061
|
+
if isinstance(stream_response_generator, dict) and 'response' in stream_response_generator:
|
|
3062
|
+
stream_generator = stream_response_generator['response']
|
|
3063
|
+
else:
|
|
3064
|
+
# Fallback for non-streaming LLM responses or errors
|
|
3065
|
+
output_content = ""
|
|
3066
|
+
if isinstance(stream_response_generator, dict) and 'output' in stream_response_generator:
|
|
3067
|
+
output_content = stream_response_generator['output']
|
|
3068
|
+
elif isinstance(stream_response_generator, str):
|
|
3069
|
+
output_content = stream_response_generator
|
|
3070
|
+
|
|
3071
|
+
yield f"data: {json.dumps({'choices': [{'delta': {'content': output_content}}]})}\n\n"
|
|
3072
|
+
yield f"data: [DONE]\n\n"
|
|
3073
|
+
return
|
|
3074
|
+
|
|
3075
|
+
|
|
3076
|
+
for response_chunk in stream_generator:
|
|
3077
|
+
with cancellation_lock:
|
|
3078
|
+
if cancellation_flags.get(current_stream_id, False):
|
|
3079
|
+
print(f"Cancellation flag triggered for {current_stream_id}. Breaking loop.")
|
|
3080
|
+
break
|
|
3081
|
+
|
|
3082
|
+
chunk_content = ""
|
|
3083
|
+
# Handle different LLM API response formats
|
|
3084
|
+
if "hf.co" in model or (provider == 'ollama' and 'gpt-oss' not in model): # Heuristic for Ollama/HF models
|
|
3085
|
+
chunk_content = response_chunk["message"]["content"] if "message" in response_chunk and "content" in response_chunk["message"] else ""
|
|
3086
|
+
else: # Assume OpenAI-like streaming format
|
|
3087
|
+
chunk_content = "".join(choice.delta.content for choice in response_chunk.choices if choice.delta.content is not None)
|
|
3088
|
+
|
|
3089
|
+
print(chunk_content, end='')
|
|
3090
|
+
|
|
3091
|
+
if chunk_content:
|
|
3092
|
+
complete_prediction.append(chunk_content)
|
|
3093
|
+
yield f"data: {json.dumps({'choices': [{'delta': {'content': chunk_content}}]})}\n\n"
|
|
3094
|
+
|
|
3095
|
+
except Exception as e:
|
|
3096
|
+
print(f"\nAn exception occurred during text prediction streaming for {current_stream_id}: {e}")
|
|
3097
|
+
traceback.print_exc()
|
|
3098
|
+
yield f"data: {json.dumps({'error': str(e)})}\n\n"
|
|
3099
|
+
|
|
3100
|
+
finally:
|
|
3101
|
+
print(f"\nText prediction stream {current_stream_id} finished.")
|
|
3102
|
+
yield f"data: [DONE]\n\n" # Signal end of stream
|
|
3103
|
+
with cancellation_lock:
|
|
3104
|
+
if current_stream_id in cancellation_flags:
|
|
3105
|
+
del cancellation_flags[current_stream_id]
|
|
3106
|
+
print(f"Cleaned up cancellation flag for stream ID: {current_stream_id}")
|
|
3107
|
+
|
|
3108
|
+
return Response(event_stream_text_predict(stream_id), mimetype="text/event-stream")
|
|
2003
3109
|
|
|
2004
3110
|
@app.route("/api/stream", methods=["POST"])
|
|
2005
3111
|
def stream():
|
|
@@ -2016,6 +3122,8 @@ def stream():
|
|
|
2016
3122
|
|
|
2017
3123
|
commandstr = data.get("commandstr")
|
|
2018
3124
|
conversation_id = data.get("conversationId")
|
|
3125
|
+
if not conversation_id:
|
|
3126
|
+
return jsonify({"error": "conversationId is required"}), 400
|
|
2019
3127
|
model = data.get("model", None)
|
|
2020
3128
|
provider = data.get("provider", None)
|
|
2021
3129
|
if provider is None:
|
|
@@ -2033,6 +3141,7 @@ def stream():
|
|
|
2033
3141
|
npc_object = None
|
|
2034
3142
|
team_object = None
|
|
2035
3143
|
team = None
|
|
3144
|
+
tool_results_for_db = []
|
|
2036
3145
|
if npc_name:
|
|
2037
3146
|
if hasattr(app, 'registered_teams'):
|
|
2038
3147
|
for team_name, team_object in app.registered_teams.items():
|
|
@@ -2195,7 +3304,9 @@ def stream():
|
|
|
2195
3304
|
if 'tools' in tool_args and tool_args['tools']:
|
|
2196
3305
|
tool_args['tool_choice'] = {"type": "auto"}
|
|
2197
3306
|
|
|
2198
|
-
|
|
3307
|
+
# Default stream response so closures below always have a value
|
|
3308
|
+
stream_response = {"output": "", "messages": messages}
|
|
3309
|
+
|
|
2199
3310
|
exe_mode = data.get('executionMode','chat')
|
|
2200
3311
|
|
|
2201
3312
|
if exe_mode == 'chat':
|
|
@@ -2269,91 +3380,260 @@ def stream():
|
|
|
2269
3380
|
)
|
|
2270
3381
|
messages = state.messages
|
|
2271
3382
|
|
|
2272
|
-
elif exe_mode == '
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
3383
|
+
elif exe_mode == 'tool_agent':
|
|
3384
|
+
mcp_server_path_from_request = data.get("mcpServerPath")
|
|
3385
|
+
selected_mcp_tools_from_request = data.get("selectedMcpTools", [])
|
|
3386
|
+
|
|
3387
|
+
# Resolve MCP server path (explicit -> team ctx -> default resolver)
|
|
3388
|
+
effective_mcp_server_path = mcp_server_path_from_request
|
|
3389
|
+
if not effective_mcp_server_path and team_object and hasattr(team_object, 'team_ctx') and team_object.team_ctx:
|
|
3390
|
+
mcp_servers_list = team_object.team_ctx.get('mcp_servers', [])
|
|
3391
|
+
if mcp_servers_list and isinstance(mcp_servers_list, list):
|
|
3392
|
+
first_server_obj = next((s for s in mcp_servers_list if isinstance(s, dict) and 'value' in s), None)
|
|
3393
|
+
if first_server_obj:
|
|
3394
|
+
effective_mcp_server_path = first_server_obj['value']
|
|
3395
|
+
elif isinstance(team_object.team_ctx.get('mcp_server'), str):
|
|
3396
|
+
effective_mcp_server_path = team_object.team_ctx.get('mcp_server')
|
|
3397
|
+
|
|
3398
|
+
effective_mcp_server_path = resolve_mcp_server_path(
|
|
3399
|
+
current_path=current_path,
|
|
3400
|
+
explicit_path=effective_mcp_server_path,
|
|
3401
|
+
force_global=False
|
|
3402
|
+
)
|
|
3403
|
+
print(f"[MCP] effective server path: {effective_mcp_server_path}")
|
|
3404
|
+
|
|
3405
|
+
if not hasattr(app, 'mcp_clients'):
|
|
3406
|
+
app.mcp_clients = {}
|
|
3407
|
+
|
|
3408
|
+
state_key = f"{conversation_id}_{npc_name or 'default'}"
|
|
3409
|
+
client_entry = app.mcp_clients.get(state_key)
|
|
3410
|
+
|
|
3411
|
+
if not client_entry or not client_entry.get("client") or not client_entry["client"].session \
|
|
3412
|
+
or client_entry.get("server_path") != effective_mcp_server_path:
|
|
3413
|
+
mcp_client = MCPClientNPC()
|
|
3414
|
+
if effective_mcp_server_path and mcp_client.connect_sync(effective_mcp_server_path):
|
|
3415
|
+
print(f"[MCP] connected client for {state_key} to {effective_mcp_server_path}")
|
|
3416
|
+
app.mcp_clients[state_key] = {
|
|
3417
|
+
"client": mcp_client,
|
|
3418
|
+
"server_path": effective_mcp_server_path,
|
|
3419
|
+
"messages": messages
|
|
3420
|
+
}
|
|
3421
|
+
else:
|
|
3422
|
+
print(f"[MCP] Failed to connect client for {state_key} to {effective_mcp_server_path}")
|
|
3423
|
+
app.mcp_clients[state_key] = {
|
|
3424
|
+
"client": None,
|
|
3425
|
+
"server_path": effective_mcp_server_path,
|
|
3426
|
+
"messages": messages
|
|
3427
|
+
}
|
|
2299
3428
|
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
3429
|
+
mcp_client = app.mcp_clients[state_key]["client"]
|
|
3430
|
+
messages = app.mcp_clients[state_key].get("messages", messages)
|
|
3431
|
+
|
|
3432
|
+
def stream_mcp_sse():
|
|
3433
|
+
nonlocal messages
|
|
3434
|
+
iteration = 0
|
|
3435
|
+
prompt = commandstr
|
|
3436
|
+
while iteration < 10:
|
|
3437
|
+
iteration += 1
|
|
3438
|
+
print(f"[MCP] iteration {iteration} prompt len={len(prompt)}")
|
|
3439
|
+
jinx_tool_catalog = {}
|
|
3440
|
+
if npc_object and hasattr(npc_object, "jinx_tool_catalog"):
|
|
3441
|
+
jinx_tool_catalog = npc_object.jinx_tool_catalog or {}
|
|
3442
|
+
tools_for_llm = []
|
|
3443
|
+
if mcp_client:
|
|
3444
|
+
tools_for_llm.extend(mcp_client.available_tools_llm)
|
|
3445
|
+
# append Jinx-derived tools
|
|
3446
|
+
tools_for_llm.extend(list(jinx_tool_catalog.values()))
|
|
3447
|
+
if selected_mcp_tools_from_request:
|
|
3448
|
+
tools_for_llm = [t for t in tools_for_llm if t["function"]["name"] in selected_mcp_tools_from_request]
|
|
3449
|
+
print(f"[MCP] tools_for_llm: {[t['function']['name'] for t in tools_for_llm]}")
|
|
3450
|
+
|
|
3451
|
+
llm_response = get_llm_response_with_handling(
|
|
3452
|
+
prompt=prompt,
|
|
2312
3453
|
npc=npc_object,
|
|
3454
|
+
messages=messages,
|
|
3455
|
+
tools=tools_for_llm,
|
|
3456
|
+
stream=True,
|
|
2313
3457
|
team=team_object,
|
|
2314
|
-
|
|
2315
|
-
mcp_server_path=effective_mcp_server_path
|
|
3458
|
+
context=f' The users working directory is {current_path}'
|
|
2316
3459
|
)
|
|
2317
|
-
app.corca_states[state_key] = corca_state
|
|
2318
|
-
else:
|
|
2319
|
-
corca_state = app.corca_states[state_key]
|
|
2320
|
-
corca_state.npc = npc_object
|
|
2321
|
-
corca_state.team = team_object
|
|
2322
|
-
corca_state.current_path = current_path
|
|
2323
|
-
corca_state.messages = messages
|
|
2324
|
-
corca_state.command_history = command_history
|
|
2325
3460
|
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
3461
|
+
stream = llm_response.get("response", [])
|
|
3462
|
+
messages = llm_response.get("messages", messages)
|
|
3463
|
+
collected_content = ""
|
|
3464
|
+
collected_tool_calls = []
|
|
3465
|
+
|
|
3466
|
+
for response_chunk in stream:
|
|
3467
|
+
with cancellation_lock:
|
|
3468
|
+
if cancellation_flags.get(stream_id, False):
|
|
3469
|
+
yield {"type": "interrupt"}
|
|
3470
|
+
return
|
|
3471
|
+
|
|
3472
|
+
if hasattr(response_chunk, "choices") and response_chunk.choices:
|
|
3473
|
+
delta = response_chunk.choices[0].delta
|
|
3474
|
+
if hasattr(delta, "content") and delta.content:
|
|
3475
|
+
collected_content += delta.content
|
|
3476
|
+
chunk_data = {
|
|
3477
|
+
"id": getattr(response_chunk, "id", None),
|
|
3478
|
+
"object": getattr(response_chunk, "object", None),
|
|
3479
|
+
"created": getattr(response_chunk, "created", datetime.datetime.now().strftime('YYYY-DD-MM-HHMMSS')),
|
|
3480
|
+
"model": getattr(response_chunk, "model", model),
|
|
3481
|
+
"choices": [
|
|
3482
|
+
{
|
|
3483
|
+
"index": 0,
|
|
3484
|
+
"delta": {
|
|
3485
|
+
"content": delta.content,
|
|
3486
|
+
"role": "assistant"
|
|
3487
|
+
},
|
|
3488
|
+
"finish_reason": None
|
|
3489
|
+
}
|
|
3490
|
+
]
|
|
3491
|
+
}
|
|
3492
|
+
yield chunk_data
|
|
3493
|
+
|
|
3494
|
+
if hasattr(delta, "tool_calls") and delta.tool_calls:
|
|
3495
|
+
for tool_call_delta in delta.tool_calls:
|
|
3496
|
+
idx = getattr(tool_call_delta, "index", 0)
|
|
3497
|
+
while len(collected_tool_calls) <= idx:
|
|
3498
|
+
collected_tool_calls.append({
|
|
3499
|
+
"id": "",
|
|
3500
|
+
"type": "function",
|
|
3501
|
+
"function": {"name": "", "arguments": ""}
|
|
3502
|
+
})
|
|
3503
|
+
if getattr(tool_call_delta, "id", None):
|
|
3504
|
+
collected_tool_calls[idx]["id"] = tool_call_delta.id
|
|
3505
|
+
if hasattr(tool_call_delta, "function"):
|
|
3506
|
+
fn = tool_call_delta.function
|
|
3507
|
+
if getattr(fn, "name", None):
|
|
3508
|
+
collected_tool_calls[idx]["function"]["name"] = fn.name
|
|
3509
|
+
if getattr(fn, "arguments", None):
|
|
3510
|
+
collected_tool_calls[idx]["function"]["arguments"] += fn.arguments
|
|
3511
|
+
|
|
3512
|
+
if not collected_tool_calls:
|
|
3513
|
+
print("[MCP] no tool calls, finishing streaming loop")
|
|
3514
|
+
break
|
|
3515
|
+
|
|
3516
|
+
print(f"[MCP] collected tool calls: {[tc['function']['name'] for tc in collected_tool_calls]}")
|
|
3517
|
+
yield {
|
|
3518
|
+
"type": "tool_execution_start",
|
|
3519
|
+
"tool_calls": [
|
|
3520
|
+
{
|
|
3521
|
+
"name": tc["function"]["name"],
|
|
3522
|
+
"id": tc["id"],
|
|
3523
|
+
"function": {
|
|
3524
|
+
"name": tc["function"]["name"],
|
|
3525
|
+
"arguments": tc["function"].get("arguments", "")
|
|
3526
|
+
}
|
|
3527
|
+
} for tc in collected_tool_calls
|
|
3528
|
+
]
|
|
3529
|
+
}
|
|
3530
|
+
|
|
3531
|
+
tool_results = []
|
|
3532
|
+
for tc in collected_tool_calls:
|
|
3533
|
+
tool_name = tc["function"]["name"]
|
|
3534
|
+
tool_args = tc["function"]["arguments"]
|
|
3535
|
+
tool_id = tc["id"]
|
|
3536
|
+
|
|
3537
|
+
if isinstance(tool_args, str):
|
|
3538
|
+
try:
|
|
3539
|
+
tool_args = json.loads(tool_args) if tool_args.strip() else {}
|
|
3540
|
+
except json.JSONDecodeError:
|
|
3541
|
+
tool_args = {}
|
|
3542
|
+
|
|
3543
|
+
print(f"[MCP] tool_start {tool_name} args={tool_args}")
|
|
3544
|
+
yield {"type": "tool_start", "name": tool_name, "id": tool_id, "args": tool_args}
|
|
3545
|
+
try:
|
|
3546
|
+
tool_content = ""
|
|
3547
|
+
# First, try local Jinx execution
|
|
3548
|
+
if npc_object and hasattr(npc_object, "jinxs_dict") and tool_name in npc_object.jinxs_dict:
|
|
3549
|
+
jinx_obj = npc_object.jinxs_dict[tool_name]
|
|
3550
|
+
try:
|
|
3551
|
+
jinx_ctx = jinx_obj.execute(
|
|
3552
|
+
input_values=tool_args if isinstance(tool_args, dict) else {},
|
|
3553
|
+
npc=npc_object,
|
|
3554
|
+
messages=messages
|
|
3555
|
+
)
|
|
3556
|
+
tool_content = str(jinx_ctx.get("output", jinx_ctx))
|
|
3557
|
+
print(f"[MCP] jinx tool_complete {tool_name}")
|
|
3558
|
+
except Exception as e:
|
|
3559
|
+
raise Exception(f"Jinx execution failed: {e}")
|
|
2340
3560
|
else:
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
3561
|
+
try:
|
|
3562
|
+
loop = asyncio.get_event_loop()
|
|
3563
|
+
except RuntimeError:
|
|
3564
|
+
loop = asyncio.new_event_loop()
|
|
3565
|
+
asyncio.set_event_loop(loop)
|
|
3566
|
+
if loop.is_closed():
|
|
3567
|
+
loop = asyncio.new_event_loop()
|
|
3568
|
+
asyncio.set_event_loop(loop)
|
|
3569
|
+
mcp_result = loop.run_until_complete(
|
|
3570
|
+
mcp_client.session.call_tool(tool_name, tool_args)
|
|
3571
|
+
) if mcp_client else {"error": "No MCP client"}
|
|
3572
|
+
if hasattr(mcp_result, "content") and mcp_result.content:
|
|
3573
|
+
for content_item in mcp_result.content:
|
|
3574
|
+
if hasattr(content_item, "text"):
|
|
3575
|
+
tool_content += content_item.text
|
|
3576
|
+
elif hasattr(content_item, "data"):
|
|
3577
|
+
tool_content += str(content_item.data)
|
|
3578
|
+
else:
|
|
3579
|
+
tool_content += str(content_item)
|
|
3580
|
+
else:
|
|
3581
|
+
tool_content = str(mcp_result)
|
|
3582
|
+
|
|
3583
|
+
tool_results.append({
|
|
3584
|
+
"role": "tool",
|
|
3585
|
+
"tool_call_id": tool_id,
|
|
3586
|
+
"name": tool_name,
|
|
3587
|
+
"content": tool_content
|
|
3588
|
+
})
|
|
3589
|
+
|
|
3590
|
+
print(f"[MCP] tool_complete {tool_name}")
|
|
3591
|
+
yield {"type": "tool_complete", "name": tool_name, "id": tool_id, "result_preview": tool_content[:4000]}
|
|
3592
|
+
except Exception as e:
|
|
3593
|
+
err_msg = f"Error executing {tool_name}: {e}"
|
|
3594
|
+
tool_results.append({
|
|
3595
|
+
"role": "tool",
|
|
3596
|
+
"tool_call_id": tool_id,
|
|
3597
|
+
"name": tool_name,
|
|
3598
|
+
"content": err_msg
|
|
3599
|
+
})
|
|
3600
|
+
print(f"[MCP] tool_error {tool_name}: {e}")
|
|
3601
|
+
yield {"type": "tool_error", "name": tool_name, "id": tool_id, "error": str(e)}
|
|
3602
|
+
|
|
3603
|
+
serialized_tool_calls = []
|
|
3604
|
+
for tc in collected_tool_calls:
|
|
3605
|
+
parsed_args = tc["function"]["arguments"]
|
|
3606
|
+
# Gemini/LLM expects arguments as JSON string, not dict
|
|
3607
|
+
if isinstance(parsed_args, dict):
|
|
3608
|
+
args_for_message = json.dumps(parsed_args)
|
|
3609
|
+
else:
|
|
3610
|
+
args_for_message = str(parsed_args)
|
|
3611
|
+
serialized_tool_calls.append({
|
|
3612
|
+
"id": tc["id"],
|
|
3613
|
+
"type": tc["type"],
|
|
3614
|
+
"function": {
|
|
3615
|
+
"name": tc["function"]["name"],
|
|
3616
|
+
"arguments": args_for_message
|
|
3617
|
+
}
|
|
3618
|
+
})
|
|
3619
|
+
|
|
3620
|
+
messages.append({
|
|
3621
|
+
"role": "assistant",
|
|
3622
|
+
"content": collected_content,
|
|
3623
|
+
"tool_calls": serialized_tool_calls
|
|
3624
|
+
})
|
|
3625
|
+
messages.extend(tool_results)
|
|
3626
|
+
tool_results_for_db = tool_results
|
|
3627
|
+
|
|
3628
|
+
prompt = ""
|
|
2356
3629
|
|
|
3630
|
+
app.mcp_clients[state_key]["messages"] = messages
|
|
3631
|
+
return
|
|
3632
|
+
|
|
3633
|
+
stream_response = stream_mcp_sse()
|
|
3634
|
+
|
|
3635
|
+
else:
|
|
3636
|
+
stream_response = {"output": f"Unsupported execution mode: {exe_mode}", "messages": messages}
|
|
2357
3637
|
|
|
2358
3638
|
user_message_filled = ''
|
|
2359
3639
|
|
|
@@ -2391,47 +3671,77 @@ def stream():
|
|
|
2391
3671
|
tool_call_data = {"id": None, "function_name": None, "arguments": ""}
|
|
2392
3672
|
|
|
2393
3673
|
try:
|
|
3674
|
+
# New: handle generators (tool_agent streaming)
|
|
3675
|
+
if hasattr(stream_response, "__iter__") and not isinstance(stream_response, (dict, str)):
|
|
3676
|
+
for chunk in stream_response:
|
|
3677
|
+
with cancellation_lock:
|
|
3678
|
+
if cancellation_flags.get(current_stream_id, False):
|
|
3679
|
+
interrupted = True
|
|
3680
|
+
break
|
|
3681
|
+
if chunk is None:
|
|
3682
|
+
continue
|
|
3683
|
+
if isinstance(chunk, dict):
|
|
3684
|
+
if chunk.get("type") == "interrupt":
|
|
3685
|
+
interrupted = True
|
|
3686
|
+
break
|
|
3687
|
+
yield f"data: {json.dumps(chunk)}\n\n"
|
|
3688
|
+
if chunk.get("choices"):
|
|
3689
|
+
for choice in chunk["choices"]:
|
|
3690
|
+
delta = choice.get("delta", {})
|
|
3691
|
+
content_piece = delta.get("content")
|
|
3692
|
+
if content_piece:
|
|
3693
|
+
complete_response.append(content_piece)
|
|
3694
|
+
continue
|
|
3695
|
+
yield f"data: {json.dumps({'choices':[{'delta':{'content': str(chunk), 'role': 'assistant'},'finish_reason':None}]})}\n\n"
|
|
3696
|
+
# ensure stream termination and cleanup for generator flows
|
|
3697
|
+
yield "data: [DONE]\n\n"
|
|
3698
|
+
with cancellation_lock:
|
|
3699
|
+
if current_stream_id in cancellation_flags:
|
|
3700
|
+
del cancellation_flags[current_stream_id]
|
|
3701
|
+
print(f"Cleaned up cancellation flag for stream ID: {current_stream_id}")
|
|
3702
|
+
return
|
|
3703
|
+
|
|
2394
3704
|
if isinstance(stream_response, str) :
|
|
2395
3705
|
print('stream a str and not a gen')
|
|
2396
3706
|
chunk_data = {
|
|
2397
|
-
"id": None,
|
|
2398
|
-
"object": None,
|
|
2399
|
-
"created": datetime.datetime.now().strftime('YYYY-DD-MM-HHMMSS'),
|
|
3707
|
+
"id": None,
|
|
3708
|
+
"object": None,
|
|
3709
|
+
"created": datetime.datetime.now().strftime('YYYY-DD-MM-HHMMSS'),
|
|
2400
3710
|
"model": model,
|
|
2401
3711
|
"choices": [
|
|
2402
3712
|
{
|
|
2403
|
-
"index": 0,
|
|
2404
|
-
"delta":
|
|
3713
|
+
"index": 0,
|
|
3714
|
+
"delta":
|
|
2405
3715
|
{
|
|
2406
3716
|
"content": stream_response,
|
|
2407
3717
|
"role": "assistant"
|
|
2408
|
-
},
|
|
3718
|
+
},
|
|
2409
3719
|
"finish_reason": 'done'
|
|
2410
3720
|
}
|
|
2411
3721
|
]
|
|
2412
3722
|
}
|
|
2413
|
-
yield f"data: {json.dumps(chunk_data)}"
|
|
3723
|
+
yield f"data: {json.dumps(chunk_data)}\n\n"
|
|
2414
3724
|
return
|
|
2415
3725
|
elif isinstance(stream_response, dict) and 'output' in stream_response and isinstance(stream_response.get('output'), str):
|
|
2416
|
-
print('stream a str and not a gen')
|
|
3726
|
+
print('stream a str and not a gen')
|
|
2417
3727
|
chunk_data = {
|
|
2418
|
-
"id": None,
|
|
2419
|
-
"object": None,
|
|
2420
|
-
"created": datetime.datetime.now().strftime('YYYY-DD-MM-HHMMSS'),
|
|
3728
|
+
"id": None,
|
|
3729
|
+
"object": None,
|
|
3730
|
+
"created": datetime.datetime.now().strftime('YYYY-DD-MM-HHMMSS'),
|
|
2421
3731
|
"model": model,
|
|
2422
3732
|
"choices": [
|
|
2423
3733
|
{
|
|
2424
|
-
"index": 0,
|
|
2425
|
-
"delta":
|
|
3734
|
+
"index": 0,
|
|
3735
|
+
"delta":
|
|
2426
3736
|
{
|
|
2427
3737
|
"content": stream_response.get('output') ,
|
|
2428
3738
|
"role": "assistant"
|
|
2429
|
-
},
|
|
3739
|
+
},
|
|
2430
3740
|
"finish_reason": 'done'
|
|
2431
3741
|
}
|
|
2432
3742
|
]
|
|
2433
3743
|
}
|
|
2434
|
-
yield f"data: {json.dumps(chunk_data)}"
|
|
3744
|
+
yield f"data: {json.dumps(chunk_data)}\n\n"
|
|
2435
3745
|
return
|
|
2436
3746
|
for response_chunk in stream_response.get('response', stream_response.get('output')):
|
|
2437
3747
|
with cancellation_lock:
|
|
@@ -2459,8 +3769,8 @@ def stream():
|
|
|
2459
3769
|
if chunk_content:
|
|
2460
3770
|
complete_response.append(chunk_content)
|
|
2461
3771
|
chunk_data = {
|
|
2462
|
-
"id": None, "object": None,
|
|
2463
|
-
"created": response_chunk["created_at"] or datetime.datetime.now(),
|
|
3772
|
+
"id": None, "object": None,
|
|
3773
|
+
"created": response_chunk["created_at"] or datetime.datetime.now(),
|
|
2464
3774
|
"model": response_chunk["model"],
|
|
2465
3775
|
"choices": [{"index": 0, "delta": {"content": chunk_content, "role": response_chunk["message"]["role"]}, "finish_reason": response_chunk.get("done_reason")}]
|
|
2466
3776
|
}
|
|
@@ -2494,33 +3804,86 @@ def stream():
|
|
|
2494
3804
|
print(f"\nAn exception occurred during streaming for {current_stream_id}: {e}")
|
|
2495
3805
|
traceback.print_exc()
|
|
2496
3806
|
interrupted = True
|
|
2497
|
-
|
|
3807
|
+
|
|
2498
3808
|
finally:
|
|
2499
3809
|
print(f"\nStream {current_stream_id} finished. Interrupted: {interrupted}")
|
|
2500
3810
|
print('\r' + ' ' * dot_count*2 + '\r', end="", flush=True)
|
|
2501
3811
|
|
|
2502
3812
|
final_response_text = ''.join(complete_response)
|
|
3813
|
+
|
|
3814
|
+
# Yield message_stop immediately so the client's stream ends quickly
|
|
2503
3815
|
yield f"data: {json.dumps({'type': 'message_stop'})}\n\n"
|
|
2504
|
-
|
|
3816
|
+
|
|
3817
|
+
# Persist tool call metadata and results before final assistant content
|
|
3818
|
+
if tool_call_data.get("function_name") or tool_call_data.get("arguments"):
|
|
3819
|
+
save_conversation_message(
|
|
3820
|
+
command_history,
|
|
3821
|
+
conversation_id,
|
|
3822
|
+
"assistant",
|
|
3823
|
+
{"tool_call": tool_call_data},
|
|
3824
|
+
wd=current_path,
|
|
3825
|
+
model=model,
|
|
3826
|
+
provider=provider,
|
|
3827
|
+
npc=npc_name,
|
|
3828
|
+
team=team,
|
|
3829
|
+
message_id=generate_message_id(),
|
|
3830
|
+
)
|
|
3831
|
+
|
|
3832
|
+
if tool_results_for_db:
|
|
3833
|
+
for tr in tool_results_for_db:
|
|
3834
|
+
save_conversation_message(
|
|
3835
|
+
command_history,
|
|
3836
|
+
conversation_id,
|
|
3837
|
+
"tool",
|
|
3838
|
+
{"tool_name": tr.get("name"), "tool_call_id": tr.get("tool_call_id"), "content": tr.get("content")},
|
|
3839
|
+
wd=current_path,
|
|
3840
|
+
model=model,
|
|
3841
|
+
provider=provider,
|
|
3842
|
+
npc=npc_name,
|
|
3843
|
+
team=team,
|
|
3844
|
+
message_id=generate_message_id(),
|
|
3845
|
+
)
|
|
3846
|
+
|
|
3847
|
+
# Save assistant message to the database
|
|
2505
3848
|
npc_name_to_save = npc_object.name if npc_object else ''
|
|
2506
3849
|
save_conversation_message(
|
|
2507
|
-
command_history,
|
|
2508
|
-
conversation_id,
|
|
2509
|
-
"assistant",
|
|
3850
|
+
command_history,
|
|
3851
|
+
conversation_id,
|
|
3852
|
+
"assistant",
|
|
2510
3853
|
final_response_text,
|
|
2511
|
-
wd=current_path,
|
|
2512
|
-
model=model,
|
|
3854
|
+
wd=current_path,
|
|
3855
|
+
model=model,
|
|
2513
3856
|
provider=provider,
|
|
2514
|
-
npc=npc_name_to_save,
|
|
2515
|
-
team=team,
|
|
3857
|
+
npc=npc_name_to_save,
|
|
3858
|
+
team=team,
|
|
2516
3859
|
message_id=message_id,
|
|
2517
3860
|
)
|
|
2518
3861
|
|
|
3862
|
+
# Start background tasks for memory extraction and context compression
|
|
3863
|
+
# These will run without blocking the main response stream.
|
|
3864
|
+
conversation_turn_text = f"User: {commandstr}\nAssistant: {final_response_text}"
|
|
3865
|
+
background_thread = threading.Thread(
|
|
3866
|
+
target=_run_stream_post_processing,
|
|
3867
|
+
args=(
|
|
3868
|
+
conversation_turn_text,
|
|
3869
|
+
conversation_id,
|
|
3870
|
+
command_history,
|
|
3871
|
+
npc_name,
|
|
3872
|
+
team, # Pass the team variable from the outer scope
|
|
3873
|
+
current_path,
|
|
3874
|
+
model,
|
|
3875
|
+
provider,
|
|
3876
|
+
npc_object,
|
|
3877
|
+
messages # Pass messages for context compression
|
|
3878
|
+
)
|
|
3879
|
+
)
|
|
3880
|
+
background_thread.daemon = True # Allow the main program to exit even if this thread is still running
|
|
3881
|
+
background_thread.start()
|
|
3882
|
+
|
|
2519
3883
|
with cancellation_lock:
|
|
2520
3884
|
if current_stream_id in cancellation_flags:
|
|
2521
3885
|
del cancellation_flags[current_stream_id]
|
|
2522
3886
|
print(f"Cleaned up cancellation flag for stream ID: {current_stream_id}")
|
|
2523
|
-
|
|
2524
3887
|
return Response(event_stream(stream_id), mimetype="text/event-stream")
|
|
2525
3888
|
|
|
2526
3889
|
@app.route('/api/delete_message', methods=['POST'])
|
|
@@ -2574,295 +3937,6 @@ def approve_memories():
|
|
|
2574
3937
|
|
|
2575
3938
|
|
|
2576
3939
|
|
|
2577
|
-
@app.route("/api/execute", methods=["POST"])
|
|
2578
|
-
def execute():
|
|
2579
|
-
data = request.json
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
stream_id = data.get("streamId")
|
|
2583
|
-
if not stream_id:
|
|
2584
|
-
import uuid
|
|
2585
|
-
stream_id = str(uuid.uuid4())
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
with cancellation_lock:
|
|
2589
|
-
cancellation_flags[stream_id] = False
|
|
2590
|
-
print(f"Starting execute stream with ID: {stream_id}")
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
commandstr = data.get("commandstr")
|
|
2594
|
-
conversation_id = data.get("conversationId")
|
|
2595
|
-
model = data.get("model", 'llama3.2')
|
|
2596
|
-
provider = data.get("provider", 'ollama')
|
|
2597
|
-
if provider is None:
|
|
2598
|
-
provider = available_models.get(model)
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
npc_name = data.get("npc", "sibiji")
|
|
2602
|
-
npc_source = data.get("npcSource", "global")
|
|
2603
|
-
team = data.get("team", None)
|
|
2604
|
-
current_path = data.get("currentPath")
|
|
2605
|
-
|
|
2606
|
-
if current_path:
|
|
2607
|
-
loaded_vars = load_project_env(current_path)
|
|
2608
|
-
print(f"Loaded project env variables for stream request: {list(loaded_vars.keys())}")
|
|
2609
|
-
|
|
2610
|
-
npc_object = None
|
|
2611
|
-
team_object = None
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
if team:
|
|
2615
|
-
print(team)
|
|
2616
|
-
if hasattr(app, 'registered_teams') and team in app.registered_teams:
|
|
2617
|
-
team_object = app.registered_teams[team]
|
|
2618
|
-
print(f"Using registered team: {team}")
|
|
2619
|
-
else:
|
|
2620
|
-
print(f"Warning: Team {team} not found in registered teams")
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
if npc_name:
|
|
2624
|
-
|
|
2625
|
-
if team and hasattr(app, 'registered_teams') and team in app.registered_teams:
|
|
2626
|
-
team_object = app.registered_teams[team]
|
|
2627
|
-
print('team', team_object)
|
|
2628
|
-
|
|
2629
|
-
if hasattr(team_object, 'npcs'):
|
|
2630
|
-
team_npcs = team_object.npcs
|
|
2631
|
-
if isinstance(team_npcs, dict):
|
|
2632
|
-
if npc_name in team_npcs:
|
|
2633
|
-
npc_object = team_npcs[npc_name]
|
|
2634
|
-
print(f"Found NPC {npc_name} in registered team {team}")
|
|
2635
|
-
elif isinstance(team_npcs, list):
|
|
2636
|
-
for npc in team_npcs:
|
|
2637
|
-
if hasattr(npc, 'name') and npc.name == npc_name:
|
|
2638
|
-
npc_object = npc
|
|
2639
|
-
print(f"Found NPC {npc_name} in registered team {team}")
|
|
2640
|
-
break
|
|
2641
|
-
|
|
2642
|
-
if not npc_object and hasattr(team_object, 'forenpc') and hasattr(team_object.forenpc, 'name'):
|
|
2643
|
-
if team_object.forenpc.name == npc_name:
|
|
2644
|
-
npc_object = team_object.forenpc
|
|
2645
|
-
print(f"Found NPC {npc_name} as forenpc in team {team}")
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
if not npc_object and hasattr(app, 'registered_npcs') and npc_name in app.registered_npcs:
|
|
2649
|
-
npc_object = app.registered_npcs[npc_name]
|
|
2650
|
-
print(f"Found NPC {npc_name} in registered NPCs")
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
if not npc_object:
|
|
2654
|
-
db_conn = get_db_connection()
|
|
2655
|
-
npc_object = load_npc_by_name_and_source(npc_name, npc_source, db_conn, current_path)
|
|
2656
|
-
|
|
2657
|
-
if not npc_object and npc_source == 'project':
|
|
2658
|
-
print(f"NPC {npc_name} not found in project directory, trying global...")
|
|
2659
|
-
npc_object = load_npc_by_name_and_source(npc_name, 'global', db_conn)
|
|
2660
|
-
|
|
2661
|
-
if npc_object:
|
|
2662
|
-
print(f"Successfully loaded NPC {npc_name} from {npc_source} directory")
|
|
2663
|
-
else:
|
|
2664
|
-
print(f"Warning: Could not load NPC {npc_name}")
|
|
2665
|
-
|
|
2666
|
-
attachments = data.get("attachments", [])
|
|
2667
|
-
command_history = CommandHistory(app.config.get('DB_PATH'))
|
|
2668
|
-
images = []
|
|
2669
|
-
attachments_loaded = []
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
if attachments:
|
|
2673
|
-
for attachment in attachments:
|
|
2674
|
-
extension = attachment["name"].split(".")[-1]
|
|
2675
|
-
extension_mapped = extension_map.get(extension.upper(), "others")
|
|
2676
|
-
file_path = os.path.expanduser("~/.npcsh/" + extension_mapped + "/" + attachment["name"])
|
|
2677
|
-
if extension_mapped == "images":
|
|
2678
|
-
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
|
2679
|
-
img = Image.open(attachment["path"])
|
|
2680
|
-
img_byte_arr = BytesIO()
|
|
2681
|
-
img.save(img_byte_arr, format="PNG")
|
|
2682
|
-
img_byte_arr.seek(0)
|
|
2683
|
-
img.save(file_path, optimize=True, quality=50)
|
|
2684
|
-
images.append(file_path)
|
|
2685
|
-
attachments_loaded.append({
|
|
2686
|
-
"name": attachment["name"], "type": extension_mapped,
|
|
2687
|
-
"data": img_byte_arr.read(), "size": os.path.getsize(file_path)
|
|
2688
|
-
})
|
|
2689
|
-
|
|
2690
|
-
messages = fetch_messages_for_conversation(conversation_id)
|
|
2691
|
-
if len(messages) == 0 and npc_object is not None:
|
|
2692
|
-
messages = [{'role': 'system', 'content': npc_object.get_system_prompt()}]
|
|
2693
|
-
elif len(messages)>0 and messages[0]['role'] != 'system' and npc_object is not None:
|
|
2694
|
-
messages.insert(0, {'role': 'system', 'content': npc_object.get_system_prompt()})
|
|
2695
|
-
elif len(messages) > 0 and npc_object is not None:
|
|
2696
|
-
messages[0]['content'] = npc_object.get_system_prompt()
|
|
2697
|
-
if npc_object is not None and messages and messages[0]['role'] == 'system':
|
|
2698
|
-
messages[0]['content'] = npc_object.get_system_prompt()
|
|
2699
|
-
|
|
2700
|
-
message_id = generate_message_id()
|
|
2701
|
-
save_conversation_message(
|
|
2702
|
-
command_history, conversation_id, "user", commandstr,
|
|
2703
|
-
wd=current_path, model=model, provider=provider, npc=npc_name,
|
|
2704
|
-
team=team, attachments=attachments_loaded, message_id=message_id,
|
|
2705
|
-
)
|
|
2706
|
-
response_gen = check_llm_command(
|
|
2707
|
-
commandstr, messages=messages, images=images, model=model,
|
|
2708
|
-
provider=provider, npc=npc_object, team=team_object, stream=True
|
|
2709
|
-
)
|
|
2710
|
-
print(response_gen)
|
|
2711
|
-
|
|
2712
|
-
message_id = generate_message_id()
|
|
2713
|
-
|
|
2714
|
-
def event_stream(current_stream_id):
|
|
2715
|
-
complete_response = []
|
|
2716
|
-
dot_count = 0
|
|
2717
|
-
interrupted = False
|
|
2718
|
-
tool_call_data = {"id": None, "function_name": None, "arguments": ""}
|
|
2719
|
-
memory_data = None
|
|
2720
|
-
|
|
2721
|
-
try:
|
|
2722
|
-
for response_chunk in stream_response.get('response', stream_response.get('output')):
|
|
2723
|
-
with cancellation_lock:
|
|
2724
|
-
if cancellation_flags.get(current_stream_id, False):
|
|
2725
|
-
print(f"Cancellation flag triggered for {current_stream_id}. Breaking loop.")
|
|
2726
|
-
interrupted = True
|
|
2727
|
-
break
|
|
2728
|
-
|
|
2729
|
-
print('.', end="", flush=True)
|
|
2730
|
-
dot_count += 1
|
|
2731
|
-
|
|
2732
|
-
if "hf.co" in model or provider == 'ollama':
|
|
2733
|
-
chunk_content = response_chunk["message"]["content"] if "message" in response_chunk and "content" in response_chunk["message"] else ""
|
|
2734
|
-
if "message" in response_chunk and "tool_calls" in response_chunk["message"]:
|
|
2735
|
-
for tool_call in response_chunk["message"]["tool_calls"]:
|
|
2736
|
-
if "id" in tool_call:
|
|
2737
|
-
tool_call_data["id"] = tool_call["id"]
|
|
2738
|
-
if "function" in tool_call:
|
|
2739
|
-
if "name" in tool_call["function"]:
|
|
2740
|
-
tool_call_data["function_name"] = tool_call["function"]["name"]
|
|
2741
|
-
if "arguments" in tool_call["function"]:
|
|
2742
|
-
arg_val = tool_call["function"]["arguments"]
|
|
2743
|
-
if isinstance(arg_val, dict):
|
|
2744
|
-
arg_val = json.dumps(arg_val)
|
|
2745
|
-
tool_call_data["arguments"] += arg_val
|
|
2746
|
-
if chunk_content:
|
|
2747
|
-
complete_response.append(chunk_content)
|
|
2748
|
-
chunk_data = {
|
|
2749
|
-
"id": None, "object": None, "created": response_chunk["created_at"], "model": response_chunk["model"],
|
|
2750
|
-
"choices": [{"index": 0, "delta": {"content": chunk_content, "role": response_chunk["message"]["role"]}, "finish_reason": response_chunk.get("done_reason")}]
|
|
2751
|
-
}
|
|
2752
|
-
yield f"data: {json.dumps(chunk_data)}\n\n"
|
|
2753
|
-
else:
|
|
2754
|
-
chunk_content = ""
|
|
2755
|
-
reasoning_content = ""
|
|
2756
|
-
for choice in response_chunk.choices:
|
|
2757
|
-
if hasattr(choice.delta, "tool_calls") and choice.delta.tool_calls:
|
|
2758
|
-
for tool_call in choice.delta.tool_calls:
|
|
2759
|
-
if tool_call.id:
|
|
2760
|
-
tool_call_data["id"] = tool_call.id
|
|
2761
|
-
if tool_call.function:
|
|
2762
|
-
if hasattr(tool_call.function, "name") and tool_call.function.name:
|
|
2763
|
-
tool_call_data["function_name"] = tool_call.function.name
|
|
2764
|
-
if hasattr(tool_call.function, "arguments") and tool_call.function.arguments:
|
|
2765
|
-
tool_call_data["arguments"] += tool_call.function.arguments
|
|
2766
|
-
for choice in response_chunk.choices:
|
|
2767
|
-
if hasattr(choice.delta, "reasoning_content"):
|
|
2768
|
-
reasoning_content += choice.delta.reasoning_content
|
|
2769
|
-
chunk_content = "".join(choice.delta.content for choice in response_chunk.choices if choice.delta.content is not None)
|
|
2770
|
-
if chunk_content:
|
|
2771
|
-
complete_response.append(chunk_content)
|
|
2772
|
-
chunk_data = {
|
|
2773
|
-
"id": response_chunk.id, "object": response_chunk.object, "created": response_chunk.created, "model": response_chunk.model,
|
|
2774
|
-
"choices": [{"index": choice.index, "delta": {"content": choice.delta.content, "role": choice.delta.role, "reasoning_content": reasoning_content if hasattr(choice.delta, "reasoning_content") else None}, "finish_reason": choice.finish_reason} for choice in response_chunk.choices]
|
|
2775
|
-
}
|
|
2776
|
-
yield f"data: {json.dumps(chunk_data)}\n\n"
|
|
2777
|
-
|
|
2778
|
-
except Exception as e:
|
|
2779
|
-
print(f"\nAn exception occurred during streaming for {current_stream_id}: {e}")
|
|
2780
|
-
traceback.print_exc()
|
|
2781
|
-
interrupted = True
|
|
2782
|
-
|
|
2783
|
-
finally:
|
|
2784
|
-
print(f"\nStream {current_stream_id} finished. Interrupted: {interrupted}")
|
|
2785
|
-
print('\r' + ' ' * dot_count*2 + '\r', end="", flush=True)
|
|
2786
|
-
|
|
2787
|
-
final_response_text = ''.join(complete_response)
|
|
2788
|
-
|
|
2789
|
-
conversation_turn_text = f"User: {commandstr}\nAssistant: {final_response_text}"
|
|
2790
|
-
|
|
2791
|
-
try:
|
|
2792
|
-
memory_examples = command_history.get_memory_examples_for_context(
|
|
2793
|
-
npc=npc_name,
|
|
2794
|
-
team=team,
|
|
2795
|
-
directory_path=current_path
|
|
2796
|
-
)
|
|
2797
|
-
|
|
2798
|
-
memory_context = format_memory_context(memory_examples)
|
|
2799
|
-
|
|
2800
|
-
facts = get_facts(
|
|
2801
|
-
conversation_turn_text,
|
|
2802
|
-
model=npc_object.model if npc_object else model,
|
|
2803
|
-
provider=npc_object.provider if npc_object else provider,
|
|
2804
|
-
npc=npc_object,
|
|
2805
|
-
context=memory_context
|
|
2806
|
-
)
|
|
2807
|
-
|
|
2808
|
-
if facts:
|
|
2809
|
-
memories_for_approval = []
|
|
2810
|
-
for i, fact in enumerate(facts):
|
|
2811
|
-
memory_id = command_history.add_memory_to_database(
|
|
2812
|
-
message_id=f"{conversation_id}_{datetime.now().strftime('%H%M%S')}_{i}",
|
|
2813
|
-
conversation_id=conversation_id,
|
|
2814
|
-
npc=npc_name or "default",
|
|
2815
|
-
team=team or "default",
|
|
2816
|
-
directory_path=current_path or "/",
|
|
2817
|
-
initial_memory=fact['statement'],
|
|
2818
|
-
status="pending_approval",
|
|
2819
|
-
model=npc_object.model if npc_object else model,
|
|
2820
|
-
provider=npc_object.provider if npc_object else provider
|
|
2821
|
-
)
|
|
2822
|
-
|
|
2823
|
-
memories_for_approval.append({
|
|
2824
|
-
"memory_id": memory_id,
|
|
2825
|
-
"content": fact['statement'],
|
|
2826
|
-
"context": f"Type: {fact.get('type', 'unknown')}, Source: {fact.get('source_text', '')}",
|
|
2827
|
-
"npc": npc_name or "default"
|
|
2828
|
-
})
|
|
2829
|
-
|
|
2830
|
-
memory_data = {
|
|
2831
|
-
"type": "memory_approval",
|
|
2832
|
-
"memories": memories_for_approval,
|
|
2833
|
-
"conversation_id": conversation_id
|
|
2834
|
-
}
|
|
2835
|
-
|
|
2836
|
-
except Exception as e:
|
|
2837
|
-
print(f"Memory generation error: {e}")
|
|
2838
|
-
|
|
2839
|
-
if memory_data:
|
|
2840
|
-
yield f"data: {json.dumps(memory_data)}\n\n"
|
|
2841
|
-
|
|
2842
|
-
yield f"data: {json.dumps({'type': 'message_stop'})}\n\n"
|
|
2843
|
-
|
|
2844
|
-
npc_name_to_save = npc_object.name if npc_object else ''
|
|
2845
|
-
save_conversation_message(
|
|
2846
|
-
command_history,
|
|
2847
|
-
conversation_id,
|
|
2848
|
-
"assistant",
|
|
2849
|
-
final_response_text,
|
|
2850
|
-
wd=current_path,
|
|
2851
|
-
model=model,
|
|
2852
|
-
provider=provider,
|
|
2853
|
-
npc=npc_name_to_save,
|
|
2854
|
-
team=team,
|
|
2855
|
-
message_id=message_id,
|
|
2856
|
-
)
|
|
2857
|
-
|
|
2858
|
-
with cancellation_lock:
|
|
2859
|
-
if current_stream_id in cancellation_flags:
|
|
2860
|
-
del cancellation_flags[current_stream_id]
|
|
2861
|
-
print(f"Cleaned up cancellation flag for stream ID: {current_stream_id}")
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
return Response(event_stream(stream_id), mimetype="text/event-stream")
|
|
2866
3940
|
|
|
2867
3941
|
@app.route("/api/interrupt", methods=["POST"])
|
|
2868
3942
|
def interrupt_stream():
|
|
@@ -3023,6 +4097,37 @@ def ollama_status():
|
|
|
3023
4097
|
return jsonify({"status": "not_found"})
|
|
3024
4098
|
|
|
3025
4099
|
|
|
4100
|
+
@app.route("/api/ollama/tool_models", methods=["GET"])
|
|
4101
|
+
def get_ollama_tool_models():
|
|
4102
|
+
"""
|
|
4103
|
+
Best-effort detection of Ollama models whose templates include tool-call support.
|
|
4104
|
+
We scan templates for tool placeholders; if none are found we assume tools are unsupported.
|
|
4105
|
+
"""
|
|
4106
|
+
try:
|
|
4107
|
+
detected = []
|
|
4108
|
+
listing = ollama.list()
|
|
4109
|
+
for model in listing.get("models", []):
|
|
4110
|
+
name = getattr(model, "model", None) or model.get("name") if isinstance(model, dict) else None
|
|
4111
|
+
if not name:
|
|
4112
|
+
continue
|
|
4113
|
+
try:
|
|
4114
|
+
details = ollama.show(name)
|
|
4115
|
+
tmpl = details.get("template") or ""
|
|
4116
|
+
if "{{- if .Tools" in tmpl or "{{- range .Tools" in tmpl or "{{- if .ToolCalls" in tmpl:
|
|
4117
|
+
detected.append(name)
|
|
4118
|
+
continue
|
|
4119
|
+
metadata = details.get("metadata") or {}
|
|
4120
|
+
if metadata.get("tools") or metadata.get("tool_calls"):
|
|
4121
|
+
detected.append(name)
|
|
4122
|
+
except Exception as inner_e:
|
|
4123
|
+
print(f"Warning: could not inspect ollama model {name} for tool support: {inner_e}")
|
|
4124
|
+
continue
|
|
4125
|
+
return jsonify({"models": detected, "error": None})
|
|
4126
|
+
except Exception as e:
|
|
4127
|
+
print(f"Error listing Ollama tool-capable models: {e}")
|
|
4128
|
+
return jsonify({"models": [], "error": str(e)}), 500
|
|
4129
|
+
|
|
4130
|
+
|
|
3026
4131
|
@app.route('/api/ollama/models', methods=['GET'])
|
|
3027
4132
|
def get_ollama_models():
|
|
3028
4133
|
response = ollama.list()
|