npcsh 1.0.14__py3-none-any.whl → 1.0.17__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.
- npcsh/_state.py +1536 -78
- npcsh/corca.py +709 -0
- npcsh/guac.py +1433 -596
- npcsh/mcp_server.py +64 -60
- npcsh/npc.py +125 -98
- npcsh/npcsh.py +41 -1318
- npcsh/pti.py +195 -215
- npcsh/routes.py +106 -36
- npcsh/spool.py +138 -144
- {npcsh-1.0.14.dist-info → npcsh-1.0.17.dist-info}/METADATA +37 -367
- npcsh-1.0.17.dist-info/RECORD +21 -0
- {npcsh-1.0.14.dist-info → npcsh-1.0.17.dist-info}/entry_points.txt +1 -1
- npcsh/mcp_npcsh.py +0 -822
- npcsh-1.0.14.dist-info/RECORD +0 -21
- {npcsh-1.0.14.dist-info → npcsh-1.0.17.dist-info}/WHEEL +0 -0
- {npcsh-1.0.14.dist-info → npcsh-1.0.17.dist-info}/licenses/LICENSE +0 -0
- {npcsh-1.0.14.dist-info → npcsh-1.0.17.dist-info}/top_level.txt +0 -0
npcsh/routes.py
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
from typing import Callable, Dict, Any, List, Optional, Union
|
|
4
4
|
import functools
|
|
5
5
|
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
6
10
|
import traceback
|
|
7
11
|
import shlex
|
|
8
12
|
import time
|
|
@@ -18,7 +22,6 @@ from npcpy.llm_funcs import (
|
|
|
18
22
|
gen_video,
|
|
19
23
|
breathe,
|
|
20
24
|
)
|
|
21
|
-
from npcpy.npc_compiler import NPC, Team, Jinx
|
|
22
25
|
from npcpy.npc_compiler import initialize_npc_project
|
|
23
26
|
from npcpy.npc_sysenv import render_markdown
|
|
24
27
|
from npcpy.work.plan import execute_plan_command
|
|
@@ -29,8 +32,7 @@ from npcpy.memory.command_history import CommandHistory, load_kg_from_db, save_k
|
|
|
29
32
|
from npcpy.serve import start_flask_server
|
|
30
33
|
from npcpy.mix.debate import run_debate
|
|
31
34
|
from npcpy.data.image import capture_screenshot
|
|
32
|
-
from npcpy.npc_compiler import NPC, Team, Jinx
|
|
33
|
-
from npcpy.npc_compiler import initialize_npc_project
|
|
35
|
+
from npcpy.npc_compiler import NPC, Team, Jinx,initialize_npc_project
|
|
34
36
|
from npcpy.data.web import search_web
|
|
35
37
|
from npcpy.memory.knowledge_graph import kg_sleep_process, kg_dream_process
|
|
36
38
|
|
|
@@ -38,6 +40,7 @@ from npcpy.memory.knowledge_graph import kg_sleep_process, kg_dream_process
|
|
|
38
40
|
from npcsh._state import (
|
|
39
41
|
NPCSH_VISION_MODEL,
|
|
40
42
|
NPCSH_VISION_PROVIDER,
|
|
43
|
+
set_npcsh_config_value,
|
|
41
44
|
NPCSH_API_URL,
|
|
42
45
|
NPCSH_CHAT_MODEL,
|
|
43
46
|
NPCSH_CHAT_PROVIDER,
|
|
@@ -55,15 +58,20 @@ from npcsh._state import (
|
|
|
55
58
|
normalize_and_expand_flags,
|
|
56
59
|
get_argument_help
|
|
57
60
|
)
|
|
61
|
+
from npcsh.corca import enter_corca_mode
|
|
58
62
|
from npcsh.guac import enter_guac_mode
|
|
59
63
|
from npcsh.plonk import execute_plonk_command, format_plonk_summary
|
|
60
64
|
from npcsh.alicanto import alicanto
|
|
65
|
+
from npcsh.pti import enter_pti_mode
|
|
61
66
|
from npcsh.spool import enter_spool_mode
|
|
62
67
|
from npcsh.wander import enter_wander_mode
|
|
63
68
|
from npcsh.yap import enter_yap_mode
|
|
64
69
|
|
|
65
70
|
|
|
66
71
|
|
|
72
|
+
NPC_STUDIO_DIR = Path.home() / ".npcsh" / "npc-studio"
|
|
73
|
+
|
|
74
|
+
|
|
67
75
|
class CommandRouter:
|
|
68
76
|
def __init__(self):
|
|
69
77
|
self.routes = {}
|
|
@@ -222,6 +230,10 @@ def compile_handler(command: str, **kwargs):
|
|
|
222
230
|
|
|
223
231
|
|
|
224
232
|
|
|
233
|
+
@router.route("corca", "Enter the Corca MCP-powered agentic shell. Usage: /corca [--mcp-server-path path]")
|
|
234
|
+
def corca_handler(command: str, **kwargs):
|
|
235
|
+
return enter_corca_mode(command=command, **kwargs)
|
|
236
|
+
|
|
225
237
|
@router.route("flush", "Flush the last N messages")
|
|
226
238
|
def flush_handler(command: str, **kwargs):
|
|
227
239
|
messages = safe_get(kwargs, "messages", [])
|
|
@@ -279,7 +291,8 @@ def guac_handler(command, **kwargs):
|
|
|
279
291
|
team = Team(npc_team_dir, db_conn=db_conn)
|
|
280
292
|
|
|
281
293
|
|
|
282
|
-
enter_guac_mode(
|
|
294
|
+
enter_guac_mode(workspace_dirs,
|
|
295
|
+
npc=npc,
|
|
283
296
|
team=team,
|
|
284
297
|
config_dir=config_dir,
|
|
285
298
|
plots_dir=plots_dir,
|
|
@@ -370,9 +383,77 @@ def init_handler(command: str, **kwargs):
|
|
|
370
383
|
output = f"Error initializing project: {e}"
|
|
371
384
|
return {"output": output, "messages": messages}
|
|
372
385
|
|
|
386
|
+
def ensure_repo():
|
|
387
|
+
"""Clone or update the npc-studio repo."""
|
|
388
|
+
if not NPC_STUDIO_DIR.exists():
|
|
389
|
+
os.makedirs(NPC_STUDIO_DIR.parent, exist_ok=True)
|
|
390
|
+
subprocess.check_call([
|
|
391
|
+
"git", "clone",
|
|
392
|
+
"https://github.com/npc-worldwide/npc-studio.git",
|
|
393
|
+
str(NPC_STUDIO_DIR)
|
|
394
|
+
])
|
|
395
|
+
else:
|
|
396
|
+
subprocess.check_call(
|
|
397
|
+
["git", "pull"],
|
|
398
|
+
cwd=NPC_STUDIO_DIR
|
|
399
|
+
)
|
|
373
400
|
|
|
401
|
+
def install_dependencies():
|
|
402
|
+
"""Install npm and pip dependencies."""
|
|
403
|
+
# Install frontend deps
|
|
404
|
+
subprocess.check_call(["npm", "install"], cwd=NPC_STUDIO_DIR)
|
|
374
405
|
|
|
406
|
+
# Install backend deps
|
|
407
|
+
req_file = NPC_STUDIO_DIR / "requirements.txt"
|
|
408
|
+
if req_file.exists():
|
|
409
|
+
subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", str(req_file)])
|
|
410
|
+
def launch_npc_studio(path_to_open: str = None):
|
|
411
|
+
"""
|
|
412
|
+
Launch the NPC Studio backend + frontend.
|
|
413
|
+
Returns PIDs for processes.
|
|
414
|
+
"""
|
|
415
|
+
ensure_repo()
|
|
416
|
+
install_dependencies()
|
|
417
|
+
|
|
418
|
+
# Start backend (Flask server)
|
|
419
|
+
backend = subprocess.Popen(
|
|
420
|
+
[sys.executable, "npc_studio_serve.py"],
|
|
421
|
+
cwd=NPC_STUDIO_DIR,
|
|
422
|
+
shell = False
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
# Start server (Electron)
|
|
426
|
+
dev_server = subprocess.Popen(
|
|
427
|
+
["npm", "run", "dev"],
|
|
428
|
+
cwd=NPC_STUDIO_DIR,
|
|
429
|
+
shell=False
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
# Start frontend (Electron)
|
|
433
|
+
frontend = subprocess.Popen(
|
|
434
|
+
["npm", "start"],
|
|
435
|
+
cwd=NPC_STUDIO_DIR,
|
|
436
|
+
shell=False
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
return backend, dev_server, frontend
|
|
440
|
+
# ========== Router handler ==========
|
|
441
|
+
@router.route("npc-studio", "Start npc studio")
|
|
442
|
+
def npc_studio_handler(command: str, **kwargs):
|
|
443
|
+
messages = kwargs.get("messages", [])
|
|
444
|
+
user_command = " ".join(command.split()[1:])
|
|
375
445
|
|
|
446
|
+
try:
|
|
447
|
+
backend, electron, frontend = launch_npc_studio(user_command or None)
|
|
448
|
+
return {
|
|
449
|
+
"output": f"NPC Studio started!\nBackend PID={backend.pid}, Electron PID={electron.pid} Frontend PID={frontend.pid}",
|
|
450
|
+
"messages": messages
|
|
451
|
+
}
|
|
452
|
+
except Exception as e:
|
|
453
|
+
return {
|
|
454
|
+
"output": f"Failed to start NPC Studio: {e}",
|
|
455
|
+
"messages": messages
|
|
456
|
+
}
|
|
376
457
|
@router.route("ots", "Take screenshot and analyze with vision model")
|
|
377
458
|
def ots_handler(command: str, **kwargs):
|
|
378
459
|
command_parts = command.split()
|
|
@@ -440,23 +521,25 @@ def ots_handler(command: str, **kwargs):
|
|
|
440
521
|
return {"output": f"Error during /ots command: {e}", "messages": messages}
|
|
441
522
|
|
|
442
523
|
|
|
524
|
+
|
|
525
|
+
|
|
443
526
|
@router.route("plan", "Execute a plan command")
|
|
444
527
|
def plan_handler(command: str, **kwargs):
|
|
445
528
|
messages = safe_get(kwargs, "messages", [])
|
|
446
529
|
user_command = " ".join(command.split()[1:])
|
|
447
530
|
if not user_command:
|
|
448
531
|
return {"output": "Usage: /plan <description_of_plan>", "messages": messages}
|
|
449
|
-
try:
|
|
450
|
-
|
|
451
|
-
except NameError:
|
|
452
|
-
return {"output": "Plan function (execute_plan_command) not available.", "messages": messages}
|
|
453
|
-
except Exception as e:
|
|
454
|
-
traceback.print_exc()
|
|
455
|
-
return {"output": f"Error executing plan: {e}", "messages": messages}
|
|
532
|
+
#try:
|
|
533
|
+
return execute_plan_command(command=user_command, **kwargs)
|
|
456
534
|
|
|
457
|
-
|
|
535
|
+
#return {"output": "Plan function (execute_plan_command) not available.", "messages": messages}
|
|
536
|
+
#except Exception as e:
|
|
537
|
+
# traceback.print_exc()
|
|
538
|
+
# return {"output": f"Error executing plan: {e}", "messages": messages}
|
|
539
|
+
|
|
540
|
+
@router.route("pti", "Enter Pardon-The-Interruption mode for human-in-the-loop reasoning.")
|
|
458
541
|
def pti_handler(command: str, **kwargs):
|
|
459
|
-
return
|
|
542
|
+
return enter_pti_mode(command=command, **kwargs)
|
|
460
543
|
|
|
461
544
|
@router.route("plonk", "Use vision model to interact with GUI. Usage: /plonk <task description>")
|
|
462
545
|
def plonk_handler(command: str, **kwargs):
|
|
@@ -500,17 +583,18 @@ def brainblast_handler(command: str, **kwargs):
|
|
|
500
583
|
parts = shlex.split(command)
|
|
501
584
|
search_query = " ".join(parts[1:]) if len(parts) > 1 else ""
|
|
502
585
|
|
|
503
|
-
|
|
504
586
|
if not search_query:
|
|
505
587
|
return {"output": "Usage: /brainblast <search_terms>", "messages": messages}
|
|
506
588
|
|
|
507
589
|
# Get the command history instance
|
|
508
590
|
command_history = kwargs.get('command_history')
|
|
509
591
|
if not command_history:
|
|
592
|
+
#print('no command history provided to brainblast')
|
|
510
593
|
# Create a new one if not provided
|
|
511
594
|
db_path = safe_get(kwargs, "history_db_path", os.path.expanduser('~/npcsh_history.db'))
|
|
512
595
|
try:
|
|
513
596
|
command_history = CommandHistory(db_path)
|
|
597
|
+
kwargs['command_history'] = command_history
|
|
514
598
|
except Exception as e:
|
|
515
599
|
return {"output": f"Error connecting to command history: {e}", "messages": messages}
|
|
516
600
|
|
|
@@ -521,13 +605,8 @@ def brainblast_handler(command: str, **kwargs):
|
|
|
521
605
|
|
|
522
606
|
# Execute the brainblast command
|
|
523
607
|
return execute_brainblast_command(
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
messages=messages,
|
|
527
|
-
top_k=safe_get(kwargs, 'top_k', 5),
|
|
528
|
-
**kwargs
|
|
529
|
-
)
|
|
530
|
-
|
|
608
|
+
command=search_query,
|
|
609
|
+
**kwargs)
|
|
531
610
|
except Exception as e:
|
|
532
611
|
traceback.print_exc()
|
|
533
612
|
return {"output": f"Error executing brainblast command: {e}", "messages": messages}
|
|
@@ -831,29 +910,18 @@ def sleep_handler(command: str, **kwargs):
|
|
|
831
910
|
@router.route("spool", "Enter interactive chat (spool) mode")
|
|
832
911
|
def spool_handler(command: str, **kwargs):
|
|
833
912
|
try:
|
|
834
|
-
# Handle NPC loading if npc is passed as a string (name)
|
|
835
913
|
npc = safe_get(kwargs, 'npc')
|
|
836
914
|
team = safe_get(kwargs, 'team')
|
|
837
915
|
|
|
838
|
-
# If npc is a string, try to load it from the team
|
|
839
916
|
if isinstance(npc, str) and team:
|
|
840
917
|
npc_name = npc
|
|
841
918
|
if npc_name in team.npcs:
|
|
842
919
|
npc = team.npcs[npc_name]
|
|
843
920
|
else:
|
|
844
921
|
return {"output": f"Error: NPC '{npc_name}' not found in team. Available NPCs: {', '.join(team.npcs.keys())}", "messages": safe_get(kwargs, "messages", [])}
|
|
845
|
-
|
|
846
|
-
return enter_spool_mode(
|
|
847
|
-
|
|
848
|
-
provider=safe_get(kwargs, 'provider', NPCSH_CHAT_PROVIDER),
|
|
849
|
-
npc=npc,
|
|
850
|
-
team=team,
|
|
851
|
-
messages=safe_get(kwargs, 'messages'),
|
|
852
|
-
conversation_id=safe_get(kwargs, 'conversation_id'),
|
|
853
|
-
stream=safe_get(kwargs, 'stream', NPCSH_STREAM_OUTPUT),
|
|
854
|
-
attachments=safe_get(kwargs, 'attachments'),
|
|
855
|
-
rag_similarity_threshold = safe_get(kwargs, 'rag_similarity_threshold', 0.3),
|
|
856
|
-
)
|
|
922
|
+
kwargs['npc'] = npc
|
|
923
|
+
return enter_spool_mode(
|
|
924
|
+
**kwargs)
|
|
857
925
|
except Exception as e:
|
|
858
926
|
traceback.print_exc()
|
|
859
927
|
return {"output": f"Error entering spool mode: {e}", "messages": safe_get(kwargs, "messages", [])}
|
|
@@ -913,13 +981,15 @@ def vixynt_handler(command: str, **kwargs):
|
|
|
913
981
|
width = safe_get(kwargs, 'width', 1024)
|
|
914
982
|
output_file = safe_get(kwargs, 'output_file')
|
|
915
983
|
attachments = safe_get(kwargs, 'attachments')
|
|
984
|
+
if isinstance(attachments, str):
|
|
985
|
+
attachments = attachments.split(',')
|
|
986
|
+
|
|
916
987
|
messages = safe_get(kwargs, 'messages', [])
|
|
917
988
|
|
|
918
989
|
user_prompt = " ".join(safe_get(kwargs, 'positional_args', []))
|
|
919
990
|
|
|
920
991
|
if not user_prompt:
|
|
921
992
|
return {"output": "Usage: /vixynt <prompt> [--output_file path] [--attachments path]", "messages": messages}
|
|
922
|
-
|
|
923
993
|
try:
|
|
924
994
|
image = gen_image(
|
|
925
995
|
prompt=user_prompt,
|
npcsh/spool.py
CHANGED
|
@@ -4,174 +4,126 @@ from npcpy.data.image import capture_screenshot
|
|
|
4
4
|
from npcpy.data.text import rag_search
|
|
5
5
|
|
|
6
6
|
import os
|
|
7
|
+
import sys
|
|
7
8
|
from npcpy.npc_sysenv import (
|
|
8
9
|
print_and_process_stream_with_markdown,
|
|
10
|
+
get_system_message,
|
|
11
|
+
render_markdown,
|
|
9
12
|
)
|
|
10
|
-
from
|
|
11
|
-
get_system_message,
|
|
12
|
-
render_markdown,
|
|
13
|
-
|
|
14
|
-
)
|
|
15
|
-
from npcsh._state import (
|
|
13
|
+
from npcsh._state import (
|
|
16
14
|
orange,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
ShellState,
|
|
16
|
+
execute_command,
|
|
17
|
+
get_multiline_input,
|
|
18
|
+
readline_safe_prompt,
|
|
19
|
+
setup_shell,
|
|
20
|
+
get_npc_path,
|
|
21
|
+
process_result,
|
|
22
|
+
initial_state,
|
|
22
23
|
)
|
|
23
|
-
from npcpy.llm_funcs import
|
|
24
|
-
|
|
24
|
+
from npcpy.llm_funcs import get_llm_response
|
|
25
25
|
from npcpy.npc_compiler import NPC
|
|
26
26
|
from typing import Any, List, Dict, Union
|
|
27
27
|
from npcsh.yap import enter_yap_mode
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
from termcolor import colored
|
|
29
|
+
def print_spool_ascii():
|
|
30
|
+
spool_art = """
|
|
31
|
+
██████╗██████╗ ████████╗ ████████╗ ██╗
|
|
32
|
+
██╔════╝██╔══██╗██╔🧵🧵🧵██ ██╔🧵🧵🧵██ ██║
|
|
33
|
+
╚█████╗ ██████╔╝██║🧵🔴🧵██ ██║🧵🔴🧵██ ██║
|
|
34
|
+
╚═══██╗██╔═══╝ ██║🧵🧵🧵██ ██║🧵🧵🧵██ ██║
|
|
35
|
+
██████╔╝██║ ██╚══════██ ██ ══════██ ██║
|
|
36
|
+
╚═════╝ ╚═╝ ╚═████████ ███████═╝ █████████╗
|
|
37
|
+
"""
|
|
38
|
+
print(spool_art)
|
|
30
39
|
def enter_spool_mode(
|
|
31
40
|
npc: NPC = None,
|
|
32
41
|
team = None,
|
|
33
42
|
model: str = None,
|
|
34
43
|
provider: str = None,
|
|
35
|
-
|
|
36
|
-
|
|
44
|
+
vmodel: str = None,
|
|
45
|
+
vprovider: str = None,
|
|
37
46
|
attachments: List[str] = None,
|
|
38
47
|
rag_similarity_threshold: float = 0.3,
|
|
39
48
|
messages: List[Dict] = None,
|
|
40
49
|
conversation_id: str = None,
|
|
41
|
-
stream: bool =
|
|
50
|
+
stream: bool = None,
|
|
42
51
|
**kwargs,
|
|
43
52
|
) -> Dict:
|
|
53
|
+
print_spool_ascii()
|
|
54
|
+
# Initialize state using existing infrastructure
|
|
55
|
+
command_history, state_team, default_npc = setup_shell()
|
|
44
56
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
57
|
+
# Create spool state, inheriting from initial_state
|
|
58
|
+
spool_state = ShellState(
|
|
59
|
+
npc=npc or default_npc,
|
|
60
|
+
team=team or state_team,
|
|
61
|
+
messages=messages.copy() if messages else [],
|
|
62
|
+
conversation_id=conversation_id or start_new_conversation(),
|
|
63
|
+
current_path=os.getcwd(),
|
|
64
|
+
stream_output=stream if stream is not None else initial_state.stream_output,
|
|
65
|
+
attachments=None,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Override models/providers if specified
|
|
69
|
+
if model:
|
|
70
|
+
spool_state.chat_model = model
|
|
71
|
+
if provider:
|
|
72
|
+
spool_state.chat_provider = provider
|
|
73
|
+
if vmodel:
|
|
74
|
+
spool_state.vision_model = vmodel
|
|
75
|
+
if vprovider:
|
|
76
|
+
spool_state.vision_provider = vprovider
|
|
77
|
+
|
|
78
|
+
npc_info = f" (NPC: {spool_state.npc.name})" if spool_state.npc else ""
|
|
79
|
+
print(f"🧵 Entering spool mode{npc_info}. Type '/sq' to exit spool mode.")
|
|
52
80
|
print("💡 Tip: Press Ctrl+C during streaming to interrupt and continue with a new message.")
|
|
53
81
|
|
|
54
|
-
|
|
82
|
+
# Handle file loading
|
|
55
83
|
loaded_chunks = {}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
command_history = CommandHistory()
|
|
61
|
-
|
|
62
|
-
files_to_load = attachments
|
|
63
|
-
if files_to_load:
|
|
64
|
-
if isinstance(files_to_load, str):
|
|
65
|
-
files_to_load = [f.strip() for f in files_to_load.split(',')]
|
|
84
|
+
if attachments:
|
|
85
|
+
if isinstance(attachments, str):
|
|
86
|
+
attachments = [f.strip() for f in attachments.split(',')]
|
|
66
87
|
|
|
67
|
-
for file_path in
|
|
88
|
+
for file_path in attachments:
|
|
68
89
|
file_path = os.path.expanduser(file_path)
|
|
69
90
|
if not os.path.exists(file_path):
|
|
70
|
-
print(f"Error: File not found at {file_path}")
|
|
91
|
+
print(colored(f"Error: File not found at {file_path}", "red"))
|
|
71
92
|
continue
|
|
72
93
|
try:
|
|
73
94
|
chunks = load_file_contents(file_path)
|
|
74
95
|
loaded_chunks[file_path] = chunks
|
|
75
|
-
print(f"Loaded {len(chunks)} chunks from: {file_path}")
|
|
96
|
+
print(colored(f"Loaded {len(chunks)} chunks from: {file_path}", "green"))
|
|
76
97
|
except Exception as e:
|
|
77
|
-
print(f"Error loading {file_path}: {str(e)}")
|
|
78
|
-
|
|
79
|
-
system_message = get_system_message(npc) if npc else "You are a helpful assistant."
|
|
80
|
-
if not spool_context or spool_context[0].get("role") != "system":
|
|
81
|
-
spool_context.insert(0, {"role": "system", "content": system_message})
|
|
82
|
-
|
|
83
|
-
if loaded_chunks:
|
|
84
|
-
initial_file_context = "\n\n--- The user has loaded the following files for this session ---\n"
|
|
85
|
-
for filename, chunks in loaded_chunks.items():
|
|
86
|
-
initial_file_context += f"\n\n--- Start of content from {filename} ---\n"
|
|
87
|
-
initial_file_context += "\n".join(chunks)
|
|
88
|
-
initial_file_context += f"\n--- End of content from {filename} ---\n"
|
|
89
|
-
|
|
90
|
-
def _handle_llm_interaction(
|
|
91
|
-
prompt,
|
|
92
|
-
current_context,
|
|
93
|
-
model_to_use,
|
|
94
|
-
provider_to_use,
|
|
95
|
-
images_to_use=None
|
|
96
|
-
):
|
|
97
|
-
|
|
98
|
-
current_context.append({"role": "user", "content": prompt})
|
|
98
|
+
print(colored(f"Error loading {file_path}: {str(e)}", "red"))
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
prompt,
|
|
105
|
-
wd=os.getcwd(),
|
|
106
|
-
model=model_to_use,
|
|
107
|
-
provider=provider_to_use,
|
|
108
|
-
npc=npc.name if npc else None,
|
|
109
|
-
team=team.name if team else None,
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
assistant_reply = ""
|
|
113
|
-
|
|
114
|
-
try:
|
|
115
|
-
response = get_llm_response(
|
|
116
|
-
prompt,
|
|
117
|
-
model=model_to_use,
|
|
118
|
-
provider=provider_to_use,
|
|
119
|
-
messages=current_context,
|
|
120
|
-
images=images_to_use,
|
|
121
|
-
stream=stream,
|
|
122
|
-
npc=npc
|
|
123
|
-
)
|
|
124
|
-
assistant_reply = response.get('response')
|
|
125
|
-
|
|
126
|
-
if stream:
|
|
127
|
-
print(orange(f'{npc.name if npc else "🧵"}....> '), end='', flush=True)
|
|
128
|
-
|
|
129
|
-
# The streaming function now handles KeyboardInterrupt internally
|
|
130
|
-
assistant_reply = print_and_process_stream_with_markdown(
|
|
131
|
-
assistant_reply,
|
|
132
|
-
model=model_to_use,
|
|
133
|
-
provider=provider_to_use
|
|
134
|
-
)
|
|
135
|
-
else:
|
|
136
|
-
render_markdown(assistant_reply)
|
|
137
|
-
|
|
138
|
-
except Exception as e:
|
|
139
|
-
assistant_reply = f"[Error during response generation: {str(e)}]"
|
|
140
|
-
print(f"\n❌ Error: {str(e)}")
|
|
141
|
-
|
|
142
|
-
current_context.append({"role": "assistant", "content": assistant_reply})
|
|
143
|
-
|
|
144
|
-
if assistant_reply and assistant_reply.count("```") % 2 != 0:
|
|
145
|
-
assistant_reply += "```"
|
|
146
|
-
|
|
147
|
-
save_conversation_message(
|
|
148
|
-
command_history,
|
|
149
|
-
conversation_id,
|
|
150
|
-
"assistant",
|
|
151
|
-
assistant_reply,
|
|
152
|
-
wd=os.getcwd(),
|
|
153
|
-
model=model_to_use,
|
|
154
|
-
provider=provider_to_use,
|
|
155
|
-
npc=npc.name if npc else None,
|
|
156
|
-
team=team.name if team else None,
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
return current_context
|
|
100
|
+
# Initialize context with system message if needed
|
|
101
|
+
if not spool_state.messages or spool_state.messages[0].get("role") != "system":
|
|
102
|
+
system_message = get_system_message(spool_state.npc) if spool_state.npc else "You are a helpful assistant."
|
|
103
|
+
spool_state.messages.insert(0, {"role": "system", "content": system_message})
|
|
160
104
|
|
|
161
105
|
while True:
|
|
162
106
|
try:
|
|
163
|
-
|
|
164
|
-
|
|
107
|
+
# Use consistent prompt styling with npcsh
|
|
108
|
+
npc_name = spool_state.npc.name if spool_state.npc else "chat"
|
|
109
|
+
display_model = spool_state.npc.model if spool_state.npc and spool_state.npc.model else spool_state.chat_model
|
|
110
|
+
|
|
111
|
+
prompt_str = f"{orange(npc_name)}:{display_model}🧵> "
|
|
112
|
+
prompt = readline_safe_prompt(prompt_str)
|
|
113
|
+
user_input = get_multiline_input(prompt).strip()
|
|
165
114
|
|
|
166
115
|
if not user_input:
|
|
167
116
|
continue
|
|
117
|
+
|
|
168
118
|
if user_input.lower() == "/sq":
|
|
169
119
|
print("Exiting spool mode.")
|
|
170
120
|
break
|
|
121
|
+
|
|
171
122
|
if user_input.lower() == "/yap":
|
|
172
|
-
|
|
123
|
+
spool_state.messages = enter_yap_mode(spool_state.messages, spool_state.npc)
|
|
173
124
|
continue
|
|
174
125
|
|
|
126
|
+
# Handle vision commands
|
|
175
127
|
if user_input.startswith("/ots"):
|
|
176
128
|
command_parts = user_input.split()
|
|
177
129
|
image_paths = []
|
|
@@ -179,26 +131,42 @@ def enter_spool_mode(
|
|
|
179
131
|
if len(command_parts) > 1:
|
|
180
132
|
for img_path in command_parts[1:]:
|
|
181
133
|
full_path = os.path.expanduser(img_path)
|
|
182
|
-
if os.path.exists(full_path):
|
|
183
|
-
|
|
134
|
+
if os.path.exists(full_path):
|
|
135
|
+
image_paths.append(full_path)
|
|
136
|
+
else:
|
|
137
|
+
print(colored(f"Error: Image file not found at {full_path}", "red"))
|
|
184
138
|
else:
|
|
185
139
|
screenshot = capture_screenshot()
|
|
186
140
|
if screenshot and "file_path" in screenshot:
|
|
187
141
|
image_paths.append(screenshot["file_path"])
|
|
188
|
-
print(f"Screenshot captured: {screenshot['filename']}")
|
|
142
|
+
print(colored(f"Screenshot captured: {screenshot['filename']}", "green"))
|
|
189
143
|
|
|
190
|
-
if not image_paths:
|
|
144
|
+
if not image_paths:
|
|
145
|
+
continue
|
|
191
146
|
|
|
192
147
|
vision_prompt = input("Prompt for image(s) (or press Enter): ").strip() or "Describe these images."
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
148
|
+
|
|
149
|
+
# Use vision models for image processing
|
|
150
|
+
response = get_llm_response(
|
|
151
|
+
vision_prompt,
|
|
152
|
+
model=spool_state.vision_model,
|
|
153
|
+
provider=spool_state.vision_provider,
|
|
154
|
+
messages=spool_state.messages,
|
|
155
|
+
images=image_paths,
|
|
156
|
+
stream=spool_state.stream_output,
|
|
157
|
+
npc=spool_state.npc,
|
|
158
|
+
**kwargs
|
|
159
|
+
|
|
199
160
|
)
|
|
161
|
+
|
|
162
|
+
spool_state.messages = response.get('messages', spool_state.messages)
|
|
163
|
+
output = response.get('response')
|
|
164
|
+
|
|
165
|
+
# Process and display the result
|
|
166
|
+
process_result(vision_prompt, spool_state, {'output': output}, command_history)
|
|
200
167
|
continue
|
|
201
168
|
|
|
169
|
+
# Handle RAG context if files are loaded
|
|
202
170
|
current_prompt = user_input
|
|
203
171
|
if loaded_chunks:
|
|
204
172
|
context_content = ""
|
|
@@ -214,24 +182,32 @@ def enter_spool_mode(
|
|
|
214
182
|
|
|
215
183
|
if context_content:
|
|
216
184
|
current_prompt += f"\n\n--- Relevant context from loaded files ---\n{context_content}"
|
|
217
|
-
print(f'prepped context_content : {context_content}')
|
|
218
185
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
186
|
+
# Use standard LLM processing
|
|
187
|
+
response = get_llm_response(
|
|
188
|
+
current_prompt,
|
|
189
|
+
model=spool_state.npc.model if spool_state.npc and spool_state.npc.model else spool_state.chat_model,
|
|
190
|
+
provider=spool_state.npc.provider if spool_state.npc and spool_state.npc.provider else spool_state.chat_provider,
|
|
191
|
+
messages=spool_state.messages,
|
|
192
|
+
stream=spool_state.stream_output,
|
|
193
|
+
npc=spool_state.npc,
|
|
194
|
+
**kwargs
|
|
224
195
|
)
|
|
196
|
+
|
|
197
|
+
spool_state.messages = response.get('messages', spool_state.messages)
|
|
198
|
+
output = response.get('response')
|
|
199
|
+
|
|
200
|
+
# Use existing result processing
|
|
201
|
+
process_result(current_prompt, spool_state, {'output': output}, command_history)
|
|
225
202
|
|
|
226
203
|
except (EOFError,):
|
|
227
204
|
print("\nExiting spool mode.")
|
|
228
205
|
break
|
|
229
206
|
except KeyboardInterrupt:
|
|
230
|
-
# This handles Ctrl+C at the input prompt (not during streaming)
|
|
231
207
|
print("\n🔄 Use '/sq' to exit or continue with a new message.")
|
|
232
208
|
continue
|
|
233
209
|
|
|
234
|
-
return {"messages":
|
|
210
|
+
return {"messages": spool_state.messages, "output": "Exited spool mode."}
|
|
235
211
|
|
|
236
212
|
|
|
237
213
|
def main():
|
|
@@ -241,14 +217,32 @@ def main():
|
|
|
241
217
|
parser.add_argument("--provider", help="Provider to use")
|
|
242
218
|
parser.add_argument("--attachments", nargs="*", help="Files to load into context")
|
|
243
219
|
parser.add_argument("--stream", default="true", help="Use streaming mode")
|
|
244
|
-
parser.add_argument("--npc", type=str,
|
|
220
|
+
parser.add_argument("--npc", type=str, help="NPC name or path to NPC file", default='sibiji',)
|
|
245
221
|
|
|
246
222
|
args = parser.parse_args()
|
|
247
223
|
|
|
248
|
-
|
|
224
|
+
# Use existing infrastructure to get NPC
|
|
225
|
+
command_history, team, default_npc = setup_shell()
|
|
226
|
+
|
|
227
|
+
npc = None
|
|
228
|
+
if args.npc:
|
|
229
|
+
if os.path.exists(os.path.expanduser(args.npc)):
|
|
230
|
+
npc = NPC(file=args.npc)
|
|
231
|
+
elif team and args.npc in team.npcs:
|
|
232
|
+
npc = team.npcs[args.npc]
|
|
233
|
+
else:
|
|
234
|
+
try:
|
|
235
|
+
npc_path = get_npc_path(args.npc, command_history.db_path)
|
|
236
|
+
npc = NPC(file=npc_path)
|
|
237
|
+
except ValueError:
|
|
238
|
+
print(colored(f"NPC '{args.npc}' not found. Using default.", "yellow"))
|
|
239
|
+
npc = default_npc
|
|
240
|
+
else:
|
|
241
|
+
npc = default_npc
|
|
249
242
|
|
|
250
243
|
enter_spool_mode(
|
|
251
244
|
npc=npc,
|
|
245
|
+
team=team,
|
|
252
246
|
model=args.model,
|
|
253
247
|
provider=args.provider,
|
|
254
248
|
attachments=args.attachments,
|