npcsh 1.0.13__py3-none-any.whl → 1.0.14__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 +23 -41
- npcsh/npcsh.py +54 -37
- npcsh/routes.py +4 -10
- npcsh/yap.py +115 -106
- {npcsh-1.0.13.dist-info → npcsh-1.0.14.dist-info}/METADATA +9 -7
- {npcsh-1.0.13.dist-info → npcsh-1.0.14.dist-info}/RECORD +10 -10
- {npcsh-1.0.13.dist-info → npcsh-1.0.14.dist-info}/WHEEL +0 -0
- {npcsh-1.0.13.dist-info → npcsh-1.0.14.dist-info}/entry_points.txt +0 -0
- {npcsh-1.0.13.dist-info → npcsh-1.0.14.dist-info}/licenses/LICENSE +0 -0
- {npcsh-1.0.13.dist-info → npcsh-1.0.14.dist-info}/top_level.txt +0 -0
npcsh/_state.py
CHANGED
|
@@ -1,45 +1,35 @@
|
|
|
1
1
|
|
|
2
2
|
from colorama import Fore, Back, Style
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
from datetime import datetime
|
|
6
|
-
from dotenv import load_dotenv
|
|
7
|
-
|
|
8
|
-
import re
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
import filecmp
|
|
9
5
|
import os
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
from typing import Dict, List, Any
|
|
15
|
-
import subprocess
|
|
16
|
-
import termios
|
|
17
|
-
import tty
|
|
6
|
+
import platform
|
|
18
7
|
import pty
|
|
8
|
+
import re
|
|
19
9
|
import select
|
|
10
|
+
import shutil
|
|
20
11
|
import signal
|
|
21
|
-
import time
|
|
22
|
-
import os
|
|
23
|
-
import re
|
|
24
12
|
import sqlite3
|
|
25
|
-
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
from termcolor import colored
|
|
16
|
+
import termios
|
|
17
|
+
import time
|
|
18
|
+
from typing import Dict, List, Any, Tuple, Union, Optional
|
|
19
|
+
import tty
|
|
26
20
|
import logging
|
|
27
21
|
import textwrap
|
|
28
22
|
from termcolor import colored
|
|
29
|
-
import
|
|
30
|
-
|
|
31
|
-
|
|
23
|
+
from npcpy.memory.command_history import (
|
|
24
|
+
start_new_conversation,
|
|
25
|
+
)
|
|
26
|
+
from npcpy.npc_compiler import NPC, Team
|
|
32
27
|
|
|
33
28
|
def get_npc_path(npc_name: str, db_path: str) -> str:
|
|
34
|
-
# First, check in project npc_team directory
|
|
35
29
|
project_npc_team_dir = os.path.abspath("./npc_team")
|
|
36
30
|
project_npc_path = os.path.join(project_npc_team_dir, f"{npc_name}.npc")
|
|
37
|
-
|
|
38
|
-
# Then, check in global npc_team directory
|
|
39
31
|
user_npc_team_dir = os.path.expanduser("~/.npcsh/npc_team")
|
|
40
32
|
global_npc_path = os.path.join(user_npc_team_dir, f"{npc_name}.npc")
|
|
41
|
-
|
|
42
|
-
# Check database for compiled NPCs
|
|
43
33
|
try:
|
|
44
34
|
with sqlite3.connect(db_path) as conn:
|
|
45
35
|
cursor = conn.cursor()
|
|
@@ -105,9 +95,6 @@ def initialize_base_npcs_if_needed(db_path: str) -> None:
|
|
|
105
95
|
package_dir = os.path.dirname(__file__)
|
|
106
96
|
package_npc_team_dir = os.path.join(package_dir, "npc_team")
|
|
107
97
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
# User's global npc_team directory
|
|
111
98
|
user_npc_team_dir = os.path.expanduser("~/.npcsh/npc_team")
|
|
112
99
|
|
|
113
100
|
user_jinxs_dir = os.path.join(user_npc_team_dir, "jinxs")
|
|
@@ -115,7 +102,7 @@ def initialize_base_npcs_if_needed(db_path: str) -> None:
|
|
|
115
102
|
os.makedirs(user_npc_team_dir, exist_ok=True)
|
|
116
103
|
os.makedirs(user_jinxs_dir, exist_ok=True)
|
|
117
104
|
os.makedirs(user_templates_dir, exist_ok=True)
|
|
118
|
-
|
|
105
|
+
|
|
119
106
|
for filename in os.listdir(package_npc_team_dir):
|
|
120
107
|
if filename.endswith(".npc"):
|
|
121
108
|
source_path = os.path.join(package_npc_team_dir, filename)
|
|
@@ -243,11 +230,11 @@ def ensure_npcshrc_exists() -> str:
|
|
|
243
230
|
npcshrc.write("# NPCSH Configuration File\n")
|
|
244
231
|
npcshrc.write("export NPCSH_INITIALIZED=0\n")
|
|
245
232
|
npcshrc.write("export NPCSH_DEFAULT_MODE='agent'\n")
|
|
233
|
+
npcshrc.write("export NPCSH_BUILD_KG=1")
|
|
246
234
|
npcshrc.write("export NPCSH_CHAT_PROVIDER='ollama'\n")
|
|
247
|
-
npcshrc.write("export NPCSH_CHAT_MODEL='
|
|
235
|
+
npcshrc.write("export NPCSH_CHAT_MODEL='gemma3:4b'\n")
|
|
248
236
|
npcshrc.write("export NPCSH_REASONING_PROVIDER='ollama'\n")
|
|
249
237
|
npcshrc.write("export NPCSH_REASONING_MODEL='deepseek-r1'\n")
|
|
250
|
-
|
|
251
238
|
npcshrc.write("export NPCSH_EMBEDDING_PROVIDER='ollama'\n")
|
|
252
239
|
npcshrc.write("export NPCSH_EMBEDDING_MODEL='nomic-embed-text'\n")
|
|
253
240
|
npcshrc.write("export NPCSH_VISION_PROVIDER='ollama'\n")
|
|
@@ -1059,12 +1046,13 @@ NPCSH_REASONING_PROVIDER = os.environ.get("NPCSH_REASONING_PROVIDER", "ollama")
|
|
|
1059
1046
|
NPCSH_STREAM_OUTPUT = eval(os.environ.get("NPCSH_STREAM_OUTPUT", "0")) == 1
|
|
1060
1047
|
NPCSH_API_URL = os.environ.get("NPCSH_API_URL", None)
|
|
1061
1048
|
NPCSH_SEARCH_PROVIDER = os.environ.get("NPCSH_SEARCH_PROVIDER", "duckduckgo")
|
|
1062
|
-
|
|
1049
|
+
NPCSH_BUILD_KG = os.environ.get("NPCSH_BUILD_KG") == "1"
|
|
1063
1050
|
READLINE_HISTORY_FILE = os.path.expanduser("~/.npcsh_history")
|
|
1064
1051
|
|
|
1065
1052
|
|
|
1066
1053
|
|
|
1067
1054
|
def setup_readline() -> str:
|
|
1055
|
+
import readline
|
|
1068
1056
|
if readline is None:
|
|
1069
1057
|
return None
|
|
1070
1058
|
try:
|
|
@@ -1097,14 +1085,6 @@ def save_readline_history():
|
|
|
1097
1085
|
|
|
1098
1086
|
|
|
1099
1087
|
|
|
1100
|
-
|
|
1101
|
-
from npcpy.memory.command_history import (
|
|
1102
|
-
start_new_conversation,
|
|
1103
|
-
)
|
|
1104
|
-
from dataclasses import dataclass, field
|
|
1105
|
-
from typing import Optional, List, Dict, Any, Tuple, Union
|
|
1106
|
-
from npcpy.npc_compiler import NPC, Team
|
|
1107
|
-
import os
|
|
1108
1088
|
@dataclass
|
|
1109
1089
|
class ShellState:
|
|
1110
1090
|
npc: Optional[Union[NPC, str]] = None
|
|
@@ -1126,6 +1106,7 @@ class ShellState:
|
|
|
1126
1106
|
video_gen_model: str = NPCSH_VIDEO_GEN_MODEL
|
|
1127
1107
|
video_gen_provider: str = NPCSH_VIDEO_GEN_PROVIDER
|
|
1128
1108
|
current_mode: str = NPCSH_DEFAULT_MODE
|
|
1109
|
+
build_kg: bool = NPCSH_BUILD_KG,
|
|
1129
1110
|
api_key: Optional[str] = None
|
|
1130
1111
|
api_url: Optional[str] = NPCSH_API_URL
|
|
1131
1112
|
current_path: str = field(default_factory=os.getcwd)
|
|
@@ -1163,5 +1144,6 @@ initial_state = ShellState(
|
|
|
1163
1144
|
image_gen_provider=NPCSH_IMAGE_GEN_PROVIDER,
|
|
1164
1145
|
video_gen_model=NPCSH_VIDEO_GEN_MODEL,
|
|
1165
1146
|
video_gen_provider=NPCSH_VIDEO_GEN_PROVIDER,
|
|
1147
|
+
build_kg=NPCSH_BUILD_KG,
|
|
1166
1148
|
api_url=NPCSH_API_URL,
|
|
1167
1149
|
)
|
npcsh/npcsh.py
CHANGED
|
@@ -35,6 +35,7 @@ from npcsh._state import (
|
|
|
35
35
|
ShellState,
|
|
36
36
|
interactive_commands,
|
|
37
37
|
BASH_COMMANDS,
|
|
38
|
+
|
|
38
39
|
start_interactive_session,
|
|
39
40
|
validate_bash_command,
|
|
40
41
|
normalize_and_expand_flags,
|
|
@@ -892,9 +893,13 @@ def execute_command(
|
|
|
892
893
|
active_provider = npc_provider or state.chat_provider
|
|
893
894
|
|
|
894
895
|
if state.current_mode == 'agent':
|
|
896
|
+
print(len(commands), commands)
|
|
895
897
|
for i, cmd_segment in enumerate(commands):
|
|
898
|
+
|
|
899
|
+
render_markdown(f'- executing command {i+1}/{len(commands)}')
|
|
896
900
|
is_last_command = (i == len(commands) - 1)
|
|
897
|
-
|
|
901
|
+
|
|
902
|
+
stream_this_segment = state.stream_output and not is_last_command
|
|
898
903
|
|
|
899
904
|
try:
|
|
900
905
|
current_state, output = process_pipeline_command(
|
|
@@ -905,17 +910,18 @@ def execute_command(
|
|
|
905
910
|
)
|
|
906
911
|
|
|
907
912
|
if is_last_command:
|
|
908
|
-
|
|
913
|
+
return current_state, output
|
|
909
914
|
if isinstance(output, str):
|
|
910
915
|
stdin_for_next = output
|
|
911
916
|
elif not isinstance(output, str):
|
|
912
917
|
try:
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
918
|
+
if stream_this_segment:
|
|
919
|
+
full_stream_output = print_and_process_stream_with_markdown(output,
|
|
920
|
+
state.npc.model,
|
|
921
|
+
state.npc.provider)
|
|
922
|
+
stdin_for_next = full_stream_output
|
|
923
|
+
if is_last_command:
|
|
924
|
+
final_output = full_stream_output
|
|
919
925
|
except:
|
|
920
926
|
if output is not None: # Try converting other types to string
|
|
921
927
|
try:
|
|
@@ -1064,6 +1070,13 @@ def setup_shell() -> Tuple[CommandHistory, Team, Optional[NPC]]:
|
|
|
1064
1070
|
command_history = CommandHistory(db_path)
|
|
1065
1071
|
|
|
1066
1072
|
|
|
1073
|
+
if not is_npcsh_initialized():
|
|
1074
|
+
print("Initializing NPCSH...")
|
|
1075
|
+
initialize_base_npcs_if_needed(db_path)
|
|
1076
|
+
print("NPCSH initialization complete. Restart or source ~/.npcshrc.")
|
|
1077
|
+
|
|
1078
|
+
|
|
1079
|
+
|
|
1067
1080
|
try:
|
|
1068
1081
|
history_file = setup_readline()
|
|
1069
1082
|
atexit.register(save_readline_history)
|
|
@@ -1260,39 +1273,42 @@ def process_result(
|
|
|
1260
1273
|
)
|
|
1261
1274
|
|
|
1262
1275
|
conversation_turn_text = f"User: {user_input}\nAssistant: {final_output_str}"
|
|
1263
|
-
|
|
1276
|
+
engine = command_history.engine
|
|
1264
1277
|
|
|
1265
|
-
try:
|
|
1266
|
-
if not should_skip_kg_processing(user_input, final_output_str):
|
|
1267
|
-
|
|
1268
|
-
npc_kg = load_kg_from_db(conn, team_name, npc_name, result_state.current_path)
|
|
1269
|
-
evolved_npc_kg, _ = kg_evolve_incremental(
|
|
1270
|
-
existing_kg=npc_kg,
|
|
1271
|
-
new_content_text=conversation_turn_text,
|
|
1272
|
-
model=active_npc.model,
|
|
1273
|
-
provider=active_npc.provider,
|
|
1274
|
-
get_concepts=True,
|
|
1275
|
-
link_concepts_facts = False,
|
|
1276
|
-
link_concepts_concepts = False,
|
|
1277
|
-
link_facts_facts = False,
|
|
1278
1278
|
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1279
|
+
if result_state.build_kg:
|
|
1280
|
+
try:
|
|
1281
|
+
if not should_skip_kg_processing(user_input, final_output_str):
|
|
1282
|
+
|
|
1283
|
+
npc_kg = load_kg_from_db(engine, team_name, npc_name, result_state.current_path)
|
|
1284
|
+
evolved_npc_kg, _ = kg_evolve_incremental(
|
|
1285
|
+
existing_kg=npc_kg,
|
|
1286
|
+
new_content_text=conversation_turn_text,
|
|
1287
|
+
model=active_npc.model,
|
|
1288
|
+
provider=active_npc.provider,
|
|
1289
|
+
get_concepts=True,
|
|
1290
|
+
link_concepts_facts = False,
|
|
1291
|
+
link_concepts_concepts = False,
|
|
1292
|
+
link_facts_facts = False,
|
|
1293
|
+
|
|
1294
|
+
|
|
1295
|
+
)
|
|
1296
|
+
save_kg_to_db(engine,
|
|
1297
|
+
evolved_npc_kg,
|
|
1298
|
+
team_name,
|
|
1299
|
+
npc_name,
|
|
1300
|
+
result_state.current_path)
|
|
1301
|
+
except Exception as e:
|
|
1302
|
+
print(colored(f"Error during real-time KG evolution: {e}", "red"))
|
|
1288
1303
|
|
|
1289
1304
|
# --- Part 3: Periodic Team Context Suggestions ---
|
|
1290
1305
|
result_state.turn_count += 1
|
|
1306
|
+
|
|
1291
1307
|
if result_state.turn_count > 0 and result_state.turn_count % 10 == 0:
|
|
1292
1308
|
print(colored("\nChecking for potential team improvements...", "cyan"))
|
|
1293
1309
|
try:
|
|
1294
1310
|
summary = breathe(messages=result_state.messages[-20:],
|
|
1295
|
-
|
|
1311
|
+
npc=active_npc)
|
|
1296
1312
|
characterization = summary.get('output')
|
|
1297
1313
|
|
|
1298
1314
|
if characterization and result_state.team:
|
|
@@ -1300,7 +1316,7 @@ def process_result(
|
|
|
1300
1316
|
ctx_data = {}
|
|
1301
1317
|
if os.path.exists(team_ctx_path):
|
|
1302
1318
|
with open(team_ctx_path, 'r') as f:
|
|
1303
|
-
|
|
1319
|
+
ctx_data = yaml.safe_load(f) or {}
|
|
1304
1320
|
current_context = ctx_data.get('context', '')
|
|
1305
1321
|
|
|
1306
1322
|
prompt = f"""Based on this characterization: {characterization},
|
|
@@ -1317,7 +1333,7 @@ def process_result(
|
|
|
1317
1333
|
|
|
1318
1334
|
if suggestion:
|
|
1319
1335
|
new_context = (current_context + " " + suggestion).strip()
|
|
1320
|
-
print(colored("
|
|
1336
|
+
print(colored(f"{result_state.npc.name} suggests updating team context:", "yellow"))
|
|
1321
1337
|
print(f" - OLD: {current_context}\n + NEW: {new_context}")
|
|
1322
1338
|
if input("Apply? [y/N]: ").strip().lower() == 'y':
|
|
1323
1339
|
ctx_data['context'] = new_context
|
|
@@ -1360,7 +1376,7 @@ def run_repl(command_history: CommandHistory, initial_state: ShellState):
|
|
|
1360
1376
|
print("\nGoodbye!")
|
|
1361
1377
|
print(colored("Processing and archiving all session knowledge...", "cyan"))
|
|
1362
1378
|
|
|
1363
|
-
|
|
1379
|
+
engine = command_history.engine
|
|
1364
1380
|
integrator_npc = NPC(name="integrator", model=current_state.chat_model, provider=current_state.chat_provider)
|
|
1365
1381
|
|
|
1366
1382
|
# Process each unique scope that was active during the session
|
|
@@ -1384,7 +1400,7 @@ def run_repl(command_history: CommandHistory, initial_state: ShellState):
|
|
|
1384
1400
|
continue
|
|
1385
1401
|
|
|
1386
1402
|
# Load the existing KG for this specific, real scope
|
|
1387
|
-
current_kg = load_kg_from_db(
|
|
1403
|
+
current_kg = load_kg_from_db(engine, team_name, npc_name, path)
|
|
1388
1404
|
|
|
1389
1405
|
# Evolve it with the full text from the session for this scope
|
|
1390
1406
|
evolved_kg, _ = kg_evolve_incremental(
|
|
@@ -1400,7 +1416,7 @@ def run_repl(command_history: CommandHistory, initial_state: ShellState):
|
|
|
1400
1416
|
)
|
|
1401
1417
|
|
|
1402
1418
|
# Save the updated KG back to the database under the same exact scope
|
|
1403
|
-
save_kg_to_db(
|
|
1419
|
+
save_kg_to_db(engine, evolved_kg, team_name, npc_name, path)
|
|
1404
1420
|
|
|
1405
1421
|
except Exception as e:
|
|
1406
1422
|
import traceback
|
|
@@ -1488,6 +1504,7 @@ def main() -> None:
|
|
|
1488
1504
|
initial_state.npc = default_npc
|
|
1489
1505
|
initial_state.team = team
|
|
1490
1506
|
|
|
1507
|
+
|
|
1491
1508
|
# add a -g global command to indicate if to use the global or project, otherwise go thru normal flow
|
|
1492
1509
|
|
|
1493
1510
|
if args.command:
|
npcsh/routes.py
CHANGED
|
@@ -757,12 +757,12 @@ def sleep_handler(command: str, **kwargs):
|
|
|
757
757
|
try:
|
|
758
758
|
db_path = os.getenv("NPCSH_DB_PATH", os.path.expanduser("~/npcsh_history.db"))
|
|
759
759
|
command_history = CommandHistory(db_path)
|
|
760
|
-
|
|
760
|
+
engine = command_history.engine
|
|
761
761
|
except Exception as e:
|
|
762
762
|
return {"output": f"Error connecting to history database for KG access: {e}", "messages": messages}
|
|
763
763
|
|
|
764
764
|
try:
|
|
765
|
-
current_kg = load_kg_from_db(
|
|
765
|
+
current_kg = load_kg_from_db(engine, team_name, npc_name, current_path)
|
|
766
766
|
|
|
767
767
|
# FIXED: Provide a detailed and helpful message when the KG is empty
|
|
768
768
|
if not current_kg or not current_kg.get('facts'):
|
|
@@ -1031,16 +1031,10 @@ def wander_handler(command: str, **kwargs):
|
|
|
1031
1031
|
return {"output": f"Error during wander mode: {e}", "messages": messages}
|
|
1032
1032
|
|
|
1033
1033
|
@router.route("yap", "Enter voice chat (yap) mode")
|
|
1034
|
-
def
|
|
1034
|
+
def yap_handler(command: str, **kwargs):
|
|
1035
1035
|
try:
|
|
1036
1036
|
return enter_yap_mode(
|
|
1037
|
-
|
|
1038
|
-
npc=safe_get(kwargs, 'npc'),
|
|
1039
|
-
model=safe_get(kwargs, 'model', NPCSH_CHAT_MODEL),
|
|
1040
|
-
provider=safe_get(kwargs, 'provider', NPCSH_CHAT_PROVIDER),
|
|
1041
|
-
team=safe_get(kwargs, 'team'),
|
|
1042
|
-
stream=safe_get(kwargs, 'stream', NPCSH_STREAM_OUTPUT),
|
|
1043
|
-
conversation_id=safe_get(kwargs, 'conversation_id')
|
|
1037
|
+
** kwargs
|
|
1044
1038
|
)
|
|
1045
1039
|
except Exception as e:
|
|
1046
1040
|
traceback.print_exc()
|
npcsh/yap.py
CHANGED
|
@@ -54,18 +54,20 @@ from npcpy.npc_compiler import (
|
|
|
54
54
|
from npcpy.memory.command_history import CommandHistory, save_conversation_message,start_new_conversation
|
|
55
55
|
from typing import Dict, Any, List
|
|
56
56
|
def enter_yap_mode(
|
|
57
|
-
|
|
58
|
-
model: str ,
|
|
59
|
-
provider: str ,
|
|
60
|
-
messages: list = None,
|
|
57
|
+
messages: list = None,
|
|
58
|
+
model: str = None,
|
|
59
|
+
provider: str = None ,
|
|
61
60
|
npc = None,
|
|
62
|
-
team= None,
|
|
61
|
+
team = None,
|
|
62
|
+
stream: bool = False,
|
|
63
|
+
api_url: str = None,
|
|
64
|
+
api_key: str=None,
|
|
65
|
+
conversation_id = None,
|
|
63
66
|
tts_model="kokoro",
|
|
64
67
|
voice="af_heart",
|
|
65
68
|
files: List[str] = None,
|
|
66
69
|
rag_similarity_threshold: float = 0.3,
|
|
67
|
-
|
|
68
|
-
conversation_id = None,
|
|
70
|
+
**kwargs
|
|
69
71
|
) -> Dict[str, Any]:
|
|
70
72
|
running = True
|
|
71
73
|
is_recording = False
|
|
@@ -100,22 +102,20 @@ def enter_yap_mode(
|
|
|
100
102
|
# Add conciseness instruction to the system message
|
|
101
103
|
system_message = system_message + " " + concise_instruction
|
|
102
104
|
|
|
103
|
-
if messages is None:
|
|
105
|
+
if messages is None or len(messages) == 0:
|
|
104
106
|
messages = [{"role": "system", "content": system_message}]
|
|
105
107
|
elif messages is not None and messages[0]['role'] != 'system':
|
|
106
108
|
messages.insert(0, {"role": "system", "content": system_message})
|
|
107
109
|
|
|
108
110
|
kokoro_pipeline = None
|
|
109
111
|
if tts_model == "kokoro":
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
112
|
+
from kokoro import KPipeline
|
|
113
|
+
import soundfile as sf
|
|
114
|
+
|
|
115
|
+
kokoro_pipeline = KPipeline(lang_code="a")
|
|
116
|
+
print("Kokoro TTS model initialized")
|
|
117
|
+
|
|
113
118
|
|
|
114
|
-
kokoro_pipeline = KPipeline(lang_code="a")
|
|
115
|
-
print("Kokoro TTS model initialized")
|
|
116
|
-
except ImportError:
|
|
117
|
-
print("Kokoro not installed, falling back to gTTS")
|
|
118
|
-
tts_model = "gtts"
|
|
119
119
|
|
|
120
120
|
# Initialize PyAudio
|
|
121
121
|
pyaudio_instance = pyaudio.PyAudio()
|
|
@@ -134,43 +134,45 @@ def enter_yap_mode(
|
|
|
134
134
|
nonlocal running, audio_stream
|
|
135
135
|
|
|
136
136
|
while running and speech_thread_active.is_set():
|
|
137
|
-
try:
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
137
|
+
#try:
|
|
138
|
+
# Get next speech item from queue
|
|
139
|
+
print('.', end='', flush=True)
|
|
140
|
+
if not speech_queue.empty():
|
|
141
|
+
print('\n')
|
|
142
|
+
text_to_speak = speech_queue.get(timeout=0.1)
|
|
143
|
+
|
|
144
|
+
# Only process if there's text to speak
|
|
145
|
+
if text_to_speak.strip():
|
|
146
|
+
# IMPORTANT: Set is_speaking flag BEFORE starting audio output
|
|
147
|
+
is_speaking.set()
|
|
148
|
+
|
|
149
|
+
# Safely close the audio input stream before speaking
|
|
150
|
+
current_audio_stream = audio_stream
|
|
151
|
+
audio_stream = (
|
|
152
|
+
None # Set to None to prevent capture thread from using it
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
if current_audio_stream and current_audio_stream.is_active():
|
|
156
|
+
current_audio_stream.stop_stream()
|
|
157
|
+
current_audio_stream.close()
|
|
158
|
+
|
|
159
|
+
print(f"Speaking full response...")
|
|
160
|
+
print(text_to_speak)
|
|
161
|
+
# Generate and play speech
|
|
162
|
+
generate_and_play_speech(text_to_speak)
|
|
163
|
+
|
|
164
|
+
# Delay after speech to prevent echo
|
|
165
|
+
time.sleep(0.005 * len(text_to_speak))
|
|
166
|
+
print(len(text_to_speak))
|
|
167
|
+
|
|
168
|
+
# Clear the speaking flag to allow listening again
|
|
169
|
+
is_speaking.clear()
|
|
170
|
+
else:
|
|
171
|
+
time.sleep(0.5)
|
|
172
|
+
#except Exception as e:
|
|
173
|
+
# print(f"Error in speech thread: {e}")
|
|
174
|
+
# is_speaking.clear() # Make sure to clear the flag if there's an error
|
|
175
|
+
# time.sleep(0.1)
|
|
174
176
|
|
|
175
177
|
def safely_close_audio_stream(stream):
|
|
176
178
|
"""Safely close an audio stream with error handling"""
|
|
@@ -315,10 +317,9 @@ def enter_yap_mode(
|
|
|
315
317
|
frames_per_buffer=CHUNK,
|
|
316
318
|
)
|
|
317
319
|
|
|
318
|
-
#
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
buffer_data = []
|
|
320
|
+
# Add timeout counter
|
|
321
|
+
timeout_counter = 0
|
|
322
|
+
max_timeout = 100 # About 10 seconds at 0.1s intervals
|
|
322
323
|
|
|
323
324
|
print("\nListening for speech...")
|
|
324
325
|
|
|
@@ -327,49 +328,63 @@ def enter_yap_mode(
|
|
|
327
328
|
and audio_stream
|
|
328
329
|
and audio_stream.is_active()
|
|
329
330
|
and not is_speaking.is_set()
|
|
331
|
+
and timeout_counter < max_timeout
|
|
330
332
|
):
|
|
331
333
|
try:
|
|
334
|
+
# Add non-blocking read with timeout
|
|
332
335
|
data = audio_stream.read(CHUNK, exception_on_overflow=False)
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
336
|
+
|
|
337
|
+
if not data:
|
|
338
|
+
timeout_counter += 1
|
|
339
|
+
time.sleep(0.1)
|
|
340
|
+
continue
|
|
341
|
+
|
|
342
|
+
# Reset timeout on successful read
|
|
343
|
+
timeout_counter = 0
|
|
344
|
+
|
|
345
|
+
audio_array = np.frombuffer(data, dtype=np.int16)
|
|
346
|
+
if len(audio_array) == 0:
|
|
347
|
+
continue
|
|
348
|
+
|
|
349
|
+
audio_float = audio_array.astype(np.float32) / 32768.0
|
|
350
|
+
tensor = torch.from_numpy(audio_float).to(device)
|
|
351
|
+
|
|
352
|
+
# Add timeout to VAD processing
|
|
353
|
+
speech_prob = vad_model(tensor, RATE).item()
|
|
354
|
+
current_time = time.time()
|
|
355
|
+
|
|
356
|
+
if speech_prob > 0.5: # VAD threshold
|
|
357
|
+
last_speech_time = current_time
|
|
358
|
+
if not is_recording:
|
|
359
|
+
is_recording = True
|
|
360
|
+
print("\nSpeech detected, listening...")
|
|
361
|
+
recording_data.extend(buffer_data)
|
|
362
|
+
buffer_data = []
|
|
363
|
+
recording_data.append(data)
|
|
364
|
+
else:
|
|
365
|
+
if is_recording:
|
|
366
|
+
if (
|
|
367
|
+
current_time - last_speech_time > 1
|
|
368
|
+
): # silence duration
|
|
369
|
+
is_recording = False
|
|
370
|
+
print("Speech ended, transcribing...")
|
|
371
|
+
|
|
372
|
+
# Stop stream before transcribing
|
|
373
|
+
safely_close_audio_stream(audio_stream)
|
|
374
|
+
audio_stream = None
|
|
375
|
+
|
|
376
|
+
# Transcribe in this thread to avoid race conditions
|
|
377
|
+
transcription = transcribe_recording(recording_data)
|
|
378
|
+
if transcription:
|
|
379
|
+
transcription_queue.put(transcription)
|
|
380
|
+
recording_data = []
|
|
381
|
+
return True # Got speech
|
|
349
382
|
else:
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
print("Speech ended, transcribing...")
|
|
356
|
-
|
|
357
|
-
# Stop stream before transcribing
|
|
358
|
-
safely_close_audio_stream(audio_stream)
|
|
359
|
-
audio_stream = None
|
|
360
|
-
|
|
361
|
-
# Transcribe in this thread to avoid race conditions
|
|
362
|
-
transcription = transcribe_recording(recording_data)
|
|
363
|
-
if transcription:
|
|
364
|
-
transcription_queue.put(transcription)
|
|
365
|
-
recording_data = []
|
|
366
|
-
return True # Got speech
|
|
367
|
-
else:
|
|
368
|
-
buffer_data.append(data)
|
|
369
|
-
if len(buffer_data) > int(
|
|
370
|
-
0.65 * RATE / CHUNK
|
|
371
|
-
): # buffer duration
|
|
372
|
-
buffer_data.pop(0)
|
|
383
|
+
buffer_data.append(data)
|
|
384
|
+
if len(buffer_data) > int(
|
|
385
|
+
0.65 * RATE / CHUNK
|
|
386
|
+
): # buffer duration
|
|
387
|
+
buffer_data.pop(0)
|
|
373
388
|
|
|
374
389
|
# Check frequently if we need to stop capturing
|
|
375
390
|
if is_speaking.is_set():
|
|
@@ -427,19 +442,14 @@ def enter_yap_mode(
|
|
|
427
442
|
|
|
428
443
|
|
|
429
444
|
while running:
|
|
430
|
-
|
|
431
|
-
# First check for typed input (non-blocking)
|
|
432
445
|
import select
|
|
433
446
|
import sys
|
|
434
|
-
|
|
435
|
-
# Don't spam the console with prompts when speaking
|
|
436
447
|
if not is_speaking.is_set():
|
|
437
448
|
print(
|
|
438
449
|
"🎤🎤🎤🎤\n Speak or type your message (or 'exit' to quit): ",
|
|
439
450
|
end="",
|
|
440
451
|
flush=True,
|
|
441
452
|
)
|
|
442
|
-
|
|
443
453
|
rlist, _, _ = select.select([sys.stdin], [], [], 0.1)
|
|
444
454
|
if rlist:
|
|
445
455
|
user_input = sys.stdin.readline().strip()
|
|
@@ -448,7 +458,7 @@ def enter_yap_mode(
|
|
|
448
458
|
break
|
|
449
459
|
if user_input:
|
|
450
460
|
print(f"\nYou (typed): {user_input}")
|
|
451
|
-
|
|
461
|
+
|
|
452
462
|
if loaded_content:
|
|
453
463
|
context_content = ""
|
|
454
464
|
for filename, content in loaded_content.items():
|
|
@@ -494,9 +504,8 @@ def enter_yap_mode(
|
|
|
494
504
|
|
|
495
505
|
|
|
496
506
|
continue # Skip audio capture this cycle
|
|
497
|
-
|
|
498
|
-
# Then try to capture some audio (if no typed input)
|
|
499
507
|
if not is_speaking.is_set(): # Only capture if not currently speaking
|
|
508
|
+
print('capturing audio')
|
|
500
509
|
got_speech = capture_audio()
|
|
501
510
|
|
|
502
511
|
# If we got speech, process it
|
|
@@ -560,9 +569,9 @@ def main():
|
|
|
560
569
|
provider = sibiji.provider
|
|
561
570
|
# Enter spool mode
|
|
562
571
|
enter_yap_mode(
|
|
563
|
-
model,
|
|
564
|
-
provider,
|
|
565
572
|
messages=None,
|
|
573
|
+
model= model,
|
|
574
|
+
provider = provider,
|
|
566
575
|
npc=sibiji,
|
|
567
576
|
team = team,
|
|
568
577
|
files=args.files,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: npcsh
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.14
|
|
4
4
|
Summary: npcsh is a command-line toolkit for using AI agents in novel ways.
|
|
5
5
|
Home-page: https://github.com/NPC-Worldwide/npcsh
|
|
6
6
|
Author: Christopher Agostino
|
|
@@ -16,6 +16,7 @@ Requires-Dist: litellm
|
|
|
16
16
|
Requires-Dist: docx
|
|
17
17
|
Requires-Dist: scipy
|
|
18
18
|
Requires-Dist: numpy
|
|
19
|
+
Requires-Dist: imagehash
|
|
19
20
|
Requires-Dist: requests
|
|
20
21
|
Requires-Dist: matplotlib
|
|
21
22
|
Requires-Dist: markdown
|
|
@@ -121,14 +122,15 @@ Once installed, the following CLI tools will be available: `npcsh`, `guac`, `npc
|
|
|
121
122
|
npcsh:🤖sibiji:gemini-2.5-flash>can you help me identify what process is listening on port 5337?
|
|
122
123
|
```
|
|
123
124
|
<p align="center">
|
|
124
|
-
<img src="https://raw.githubusercontent.com/npc-worldwide/npcsh/main/test_data/port5337.png" alt="example of running npcsh to check what processes are listening on port 5337", width=
|
|
125
|
+
<img src="https://raw.githubusercontent.com/npc-worldwide/npcsh/main/test_data/port5337.png" alt="example of running npcsh to check what processes are listening on port 5337", width=600>
|
|
125
126
|
</p>
|
|
126
127
|
- Edit files
|
|
127
128
|
|
|
128
129
|
- **Ask a Generic Question**
|
|
129
130
|
```bash
|
|
130
|
-
|
|
131
|
+
npcsh> has there ever been a better pasta shape than bucatini?
|
|
131
132
|
```
|
|
133
|
+
|
|
132
134
|
```
|
|
133
135
|
.Loaded .env file...
|
|
134
136
|
Initializing database schema...
|
|
@@ -147,7 +149,7 @@ Once installed, the following CLI tools will be available: `npcsh`, `guac`, `npc
|
|
|
147
149
|
/search "cal golden bears football schedule" -sp perplexity
|
|
148
150
|
```
|
|
149
151
|
<p align="center">
|
|
150
|
-
<img src="https://raw.githubusercontent.com/npc-worldwide/npcsh/main/test_data/search_example.png" alt="example of search results", width=
|
|
152
|
+
<img src="https://raw.githubusercontent.com/npc-worldwide/npcsh/main/test_data/search_example.png" alt="example of search results", width=600>
|
|
151
153
|
</p>
|
|
152
154
|
|
|
153
155
|
- **Computer Use**
|
|
@@ -203,10 +205,10 @@ Once installed, the following CLI tools will be available: `npcsh`, `guac`, `npc
|
|
|
203
205
|
- `/serve` - Serve an NPC Team server.
|
|
204
206
|
- `/set` - Set configuration values
|
|
205
207
|
- `/sleep` - Evolve knowledge graph with options for dreaming.
|
|
206
|
-
- `/spool` - Enter interactive chat (spool) mode
|
|
208
|
+
- `/spool` - Enter interactive chat (spool) mode with an npc with fresh context or files for rag
|
|
207
209
|
- `/trigger` - Execute a trigger command
|
|
208
|
-
- `/vixynt` - Generate images from text descriptions
|
|
209
|
-
- `/wander` -
|
|
210
|
+
- `/vixynt` - Generate and edit images from text descriptions using local models, openai, gemini
|
|
211
|
+
- `/wander` - A method for LLMs to think on a problem by switching between states of high temperature and low temperature
|
|
210
212
|
- `/yap` - Enter voice chat (yap) mode
|
|
211
213
|
|
|
212
214
|
## Common Command-Line Flags\n\nThe shortest unambiguous prefix works (e.g., `-t` for `--temperature`).
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
npcsh/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
npcsh/_state.py,sha256=
|
|
2
|
+
npcsh/_state.py,sha256=Xm2JoXLW4GNEbdME096XLzRCIIPfaNslh8VHc6JshHc,33436
|
|
3
3
|
npcsh/alicanto.py,sha256=-muGqd0O2m8xcFBctEavSEizWbQmzuPSdcT-3YqYBhY,45043
|
|
4
4
|
npcsh/guac.py,sha256=Ocmk_c4NUtGsC3JOtmkbgLvD6u-XtBPRFRYcckpgUJU,33099
|
|
5
5
|
npcsh/mcp_helpers.py,sha256=Ktd2yXuBnLL2P7OMalgGLj84PXJSzaucjqmJVvWx6HA,12723
|
|
6
6
|
npcsh/mcp_npcsh.py,sha256=SfmplH62GS9iI6q4vuQLVUS6tkrok6L7JxODx_iH7ps,36158
|
|
7
7
|
npcsh/mcp_server.py,sha256=l2Ra0lpFrUu334pvp0Q9ajF2n73KvZswFi0FgbDhh9k,5884
|
|
8
8
|
npcsh/npc.py,sha256=7ujKrMQFgkeGJ4sX5Kn_dB5tjrPN58xeC91PNt453aM,7827
|
|
9
|
-
npcsh/npcsh.py,sha256=
|
|
9
|
+
npcsh/npcsh.py,sha256=uxs_5k-zmuDjdvKMxoBZwdefdgKGESd-EIGCXYNgx0Y,59571
|
|
10
10
|
npcsh/plonk.py,sha256=7w7J2bht5QXOyV2UK045nAPDmrSrTGLX-sh56KQ3-k0,14653
|
|
11
11
|
npcsh/pti.py,sha256=jGHGE5SeIcDkV8WlOEHCKQCnYAL4IPS-kUBHrUz0oDA,10019
|
|
12
|
-
npcsh/routes.py,sha256=
|
|
12
|
+
npcsh/routes.py,sha256=gsKHhdbzcZnE91w86ydr3ABNTxL12Ta0Oa9z3qwEh54,44470
|
|
13
13
|
npcsh/spool.py,sha256=QF1SuIhj_PWiOYNkAK31f1W_wS8yYxC5XvM2GU7VJMM,9495
|
|
14
14
|
npcsh/wander.py,sha256=BiN6eYyFnEsFzo8MFLRkdZ8xS9sTKkQpjiCcy9chMcc,23225
|
|
15
|
-
npcsh/yap.py,sha256=
|
|
16
|
-
npcsh-1.0.
|
|
17
|
-
npcsh-1.0.
|
|
18
|
-
npcsh-1.0.
|
|
19
|
-
npcsh-1.0.
|
|
20
|
-
npcsh-1.0.
|
|
21
|
-
npcsh-1.0.
|
|
15
|
+
npcsh/yap.py,sha256=ipkY3uMDw8gNrPSZ9qJFWVQ_fXtLmQ2oz_6_WZt2hew,21097
|
|
16
|
+
npcsh-1.0.14.dist-info/licenses/LICENSE,sha256=IKBvAECHP-aCiJtE4cHGCE5Yl0tozYz02PomGeWS3y4,1070
|
|
17
|
+
npcsh-1.0.14.dist-info/METADATA,sha256=W0GyU5aR_MP9qUrp7BHWV54p0cgNNrY6E7wzChyvU_4,31027
|
|
18
|
+
npcsh-1.0.14.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
19
|
+
npcsh-1.0.14.dist-info/entry_points.txt,sha256=qxOYTm3ym3JWyWf2nv2Mk71uMcJIdUoNEJ8VYMkyHiY,214
|
|
20
|
+
npcsh-1.0.14.dist-info/top_level.txt,sha256=kHSNgKMCkfjV95-DH0YSp1LLBi0HXdF3w57j7MQON3E,6
|
|
21
|
+
npcsh-1.0.14.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|