npcsh 0.3.31__py3-none-any.whl → 1.0.0__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 +942 -0
- npcsh/alicanto.py +1074 -0
- npcsh/guac.py +785 -0
- npcsh/mcp_helpers.py +357 -0
- npcsh/mcp_npcsh.py +822 -0
- npcsh/mcp_server.py +184 -0
- npcsh/npc.py +218 -0
- npcsh/npcsh.py +1161 -0
- npcsh/plonk.py +387 -269
- npcsh/pti.py +234 -0
- npcsh/routes.py +958 -0
- npcsh/spool.py +315 -0
- npcsh/wander.py +550 -0
- npcsh/yap.py +573 -0
- npcsh-1.0.0.dist-info/METADATA +596 -0
- npcsh-1.0.0.dist-info/RECORD +21 -0
- {npcsh-0.3.31.dist-info → npcsh-1.0.0.dist-info}/WHEEL +1 -1
- npcsh-1.0.0.dist-info/entry_points.txt +9 -0
- {npcsh-0.3.31.dist-info → npcsh-1.0.0.dist-info}/licenses/LICENSE +1 -1
- npcsh/audio.py +0 -210
- npcsh/cli.py +0 -545
- npcsh/command_history.py +0 -566
- npcsh/conversation.py +0 -291
- npcsh/data_models.py +0 -46
- npcsh/dataframes.py +0 -163
- npcsh/embeddings.py +0 -168
- npcsh/helpers.py +0 -641
- npcsh/image.py +0 -298
- npcsh/image_gen.py +0 -79
- npcsh/knowledge_graph.py +0 -1006
- npcsh/llm_funcs.py +0 -2027
- npcsh/load_data.py +0 -83
- npcsh/main.py +0 -5
- npcsh/model_runner.py +0 -189
- npcsh/npc_compiler.py +0 -2870
- npcsh/npc_sysenv.py +0 -383
- npcsh/npc_team/assembly_lines/test_pipeline.py +0 -181
- npcsh/npc_team/corca.npc +0 -13
- npcsh/npc_team/foreman.npc +0 -7
- npcsh/npc_team/npcsh.ctx +0 -11
- npcsh/npc_team/sibiji.npc +0 -4
- npcsh/npc_team/templates/analytics/celona.npc +0 -0
- npcsh/npc_team/templates/hr_support/raone.npc +0 -0
- npcsh/npc_team/templates/humanities/eriane.npc +0 -4
- npcsh/npc_team/templates/it_support/lineru.npc +0 -0
- npcsh/npc_team/templates/marketing/slean.npc +0 -4
- npcsh/npc_team/templates/philosophy/maurawa.npc +0 -0
- npcsh/npc_team/templates/sales/turnic.npc +0 -4
- npcsh/npc_team/templates/software/welxor.npc +0 -0
- npcsh/npc_team/tools/bash_executer.tool +0 -32
- npcsh/npc_team/tools/calculator.tool +0 -8
- npcsh/npc_team/tools/code_executor.tool +0 -16
- npcsh/npc_team/tools/generic_search.tool +0 -27
- npcsh/npc_team/tools/image_generation.tool +0 -25
- npcsh/npc_team/tools/local_search.tool +0 -149
- npcsh/npc_team/tools/npcsh_executor.tool +0 -9
- npcsh/npc_team/tools/screen_cap.tool +0 -27
- npcsh/npc_team/tools/sql_executor.tool +0 -26
- npcsh/response.py +0 -623
- npcsh/search.py +0 -248
- npcsh/serve.py +0 -1460
- npcsh/shell.py +0 -538
- npcsh/shell_helpers.py +0 -3529
- npcsh/stream.py +0 -700
- npcsh/video.py +0 -49
- npcsh-0.3.31.data/data/npcsh/npc_team/bash_executer.tool +0 -32
- npcsh-0.3.31.data/data/npcsh/npc_team/calculator.tool +0 -8
- npcsh-0.3.31.data/data/npcsh/npc_team/celona.npc +0 -0
- npcsh-0.3.31.data/data/npcsh/npc_team/code_executor.tool +0 -16
- npcsh-0.3.31.data/data/npcsh/npc_team/corca.npc +0 -13
- npcsh-0.3.31.data/data/npcsh/npc_team/eriane.npc +0 -4
- npcsh-0.3.31.data/data/npcsh/npc_team/foreman.npc +0 -7
- npcsh-0.3.31.data/data/npcsh/npc_team/generic_search.tool +0 -27
- npcsh-0.3.31.data/data/npcsh/npc_team/image_generation.tool +0 -25
- npcsh-0.3.31.data/data/npcsh/npc_team/lineru.npc +0 -0
- npcsh-0.3.31.data/data/npcsh/npc_team/local_search.tool +0 -149
- npcsh-0.3.31.data/data/npcsh/npc_team/maurawa.npc +0 -0
- npcsh-0.3.31.data/data/npcsh/npc_team/npcsh.ctx +0 -11
- npcsh-0.3.31.data/data/npcsh/npc_team/npcsh_executor.tool +0 -9
- npcsh-0.3.31.data/data/npcsh/npc_team/raone.npc +0 -0
- npcsh-0.3.31.data/data/npcsh/npc_team/screen_cap.tool +0 -27
- npcsh-0.3.31.data/data/npcsh/npc_team/sibiji.npc +0 -4
- npcsh-0.3.31.data/data/npcsh/npc_team/slean.npc +0 -4
- npcsh-0.3.31.data/data/npcsh/npc_team/sql_executor.tool +0 -26
- npcsh-0.3.31.data/data/npcsh/npc_team/test_pipeline.py +0 -181
- npcsh-0.3.31.data/data/npcsh/npc_team/turnic.npc +0 -4
- npcsh-0.3.31.data/data/npcsh/npc_team/welxor.npc +0 -0
- npcsh-0.3.31.dist-info/METADATA +0 -1853
- npcsh-0.3.31.dist-info/RECORD +0 -76
- npcsh-0.3.31.dist-info/entry_points.txt +0 -3
- {npcsh-0.3.31.dist-info → npcsh-1.0.0.dist-info}/top_level.txt +0 -0
npcsh/shell_helpers.py
DELETED
|
@@ -1,3529 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import pandas as pd
|
|
3
|
-
|
|
4
|
-
from typing import Dict, Any, List, Optional, Union
|
|
5
|
-
import numpy as np
|
|
6
|
-
import readline
|
|
7
|
-
from colorama import Fore, Back, Style
|
|
8
|
-
import re
|
|
9
|
-
import tempfile
|
|
10
|
-
import sqlite3
|
|
11
|
-
import wave
|
|
12
|
-
import datetime
|
|
13
|
-
import glob
|
|
14
|
-
import shlex
|
|
15
|
-
import logging
|
|
16
|
-
import textwrap
|
|
17
|
-
import subprocess
|
|
18
|
-
from termcolor import colored
|
|
19
|
-
import sys
|
|
20
|
-
import termios
|
|
21
|
-
import tty
|
|
22
|
-
import pty
|
|
23
|
-
import select
|
|
24
|
-
import signal
|
|
25
|
-
import platform
|
|
26
|
-
import time
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
try:
|
|
30
|
-
import whisper
|
|
31
|
-
except:
|
|
32
|
-
print(
|
|
33
|
-
"Could not load the whisper package. If you want to use tts/stt features, please run `pip install npcsh[audio]` and follow the instructions in the npcsh github readme to ensure your OS can handle the audio dependencies."
|
|
34
|
-
)
|
|
35
|
-
try:
|
|
36
|
-
from sentence_transformers import SentenceTransformer
|
|
37
|
-
except:
|
|
38
|
-
|
|
39
|
-
print(
|
|
40
|
-
"Could not load the sentence-transformers package. If you want to use it or other local AI features, please run `pip install npcsh[local]` ."
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
from .load_data import load_pdf, load_csv, load_json, load_excel, load_txt, load_image
|
|
44
|
-
from .npc_sysenv import (
|
|
45
|
-
get_model_and_provider,
|
|
46
|
-
get_available_models,
|
|
47
|
-
get_system_message,
|
|
48
|
-
NPCSH_STREAM_OUTPUT,
|
|
49
|
-
NPCSH_API_URL,
|
|
50
|
-
NPCSH_CHAT_MODEL,
|
|
51
|
-
NPCSH_CHAT_PROVIDER,
|
|
52
|
-
NPCSH_VISION_MODEL,
|
|
53
|
-
NPCSH_VISION_PROVIDER,
|
|
54
|
-
NPCSH_IMAGE_GEN_MODEL,
|
|
55
|
-
NPCSH_IMAGE_GEN_PROVIDER,
|
|
56
|
-
)
|
|
57
|
-
from .command_history import (
|
|
58
|
-
CommandHistory,
|
|
59
|
-
save_attachment_to_message,
|
|
60
|
-
save_conversation_message,
|
|
61
|
-
start_new_conversation,
|
|
62
|
-
)
|
|
63
|
-
from .embeddings import search_similar_texts, chroma_client
|
|
64
|
-
|
|
65
|
-
from .llm_funcs import (
|
|
66
|
-
execute_llm_command,
|
|
67
|
-
execute_llm_question,
|
|
68
|
-
get_stream,
|
|
69
|
-
get_conversation,
|
|
70
|
-
get_llm_response,
|
|
71
|
-
check_llm_command,
|
|
72
|
-
generate_image,
|
|
73
|
-
get_embeddings,
|
|
74
|
-
get_stream,
|
|
75
|
-
)
|
|
76
|
-
from .plonk import plonk, action_space
|
|
77
|
-
from .helpers import get_db_npcs, get_npc_path
|
|
78
|
-
|
|
79
|
-
from .npc_compiler import (
|
|
80
|
-
NPCCompiler,
|
|
81
|
-
NPC,
|
|
82
|
-
load_npc_from_file,
|
|
83
|
-
PipelineRunner,
|
|
84
|
-
Tool,
|
|
85
|
-
initialize_npc_project,
|
|
86
|
-
)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
from .search import rag_search, search_web
|
|
90
|
-
from .image import capture_screenshot, analyze_image
|
|
91
|
-
|
|
92
|
-
from .audio import calibrate_silence, record_audio, speak_text
|
|
93
|
-
from rich.console import Console
|
|
94
|
-
from rich.markdown import Markdown
|
|
95
|
-
from rich.syntax import Syntax
|
|
96
|
-
import warnings
|
|
97
|
-
|
|
98
|
-
warnings.filterwarnings("ignore", module="whisper.transcribe")
|
|
99
|
-
|
|
100
|
-
warnings.filterwarnings("ignore", category=FutureWarning)
|
|
101
|
-
warnings.filterwarnings("ignore", module="torch.serialization")
|
|
102
|
-
os.environ["PYTHONWARNINGS"] = "ignore"
|
|
103
|
-
os.environ["SDL_AUDIODRIVER"] = "dummy"
|
|
104
|
-
|
|
105
|
-
interactive_commands = {
|
|
106
|
-
"ipython": ["ipython"],
|
|
107
|
-
"python": ["python", "-i"],
|
|
108
|
-
"sqlite3": ["sqlite3"],
|
|
109
|
-
"r": ["R", "--interactive"],
|
|
110
|
-
}
|
|
111
|
-
BASH_COMMANDS = [
|
|
112
|
-
"npc",
|
|
113
|
-
"npm",
|
|
114
|
-
"npx",
|
|
115
|
-
"open",
|
|
116
|
-
"alias",
|
|
117
|
-
"bg",
|
|
118
|
-
"bind",
|
|
119
|
-
"break",
|
|
120
|
-
"builtin",
|
|
121
|
-
"case",
|
|
122
|
-
"command",
|
|
123
|
-
"compgen",
|
|
124
|
-
"complete",
|
|
125
|
-
"continue",
|
|
126
|
-
"declare",
|
|
127
|
-
"dirs",
|
|
128
|
-
"disown",
|
|
129
|
-
"echo",
|
|
130
|
-
"enable",
|
|
131
|
-
"eval",
|
|
132
|
-
"exec",
|
|
133
|
-
"exit",
|
|
134
|
-
"export",
|
|
135
|
-
"fc",
|
|
136
|
-
"fg",
|
|
137
|
-
"getopts",
|
|
138
|
-
"hash",
|
|
139
|
-
"help",
|
|
140
|
-
"history",
|
|
141
|
-
"if",
|
|
142
|
-
"jobs",
|
|
143
|
-
"kill",
|
|
144
|
-
"let",
|
|
145
|
-
"local",
|
|
146
|
-
"logout",
|
|
147
|
-
"ollama",
|
|
148
|
-
"popd",
|
|
149
|
-
"printf",
|
|
150
|
-
"pushd",
|
|
151
|
-
"pwd",
|
|
152
|
-
"read",
|
|
153
|
-
"readonly",
|
|
154
|
-
"return",
|
|
155
|
-
"set",
|
|
156
|
-
"shift",
|
|
157
|
-
"shopt",
|
|
158
|
-
"source",
|
|
159
|
-
"suspend",
|
|
160
|
-
"test",
|
|
161
|
-
"times",
|
|
162
|
-
"trap",
|
|
163
|
-
"type",
|
|
164
|
-
"typeset",
|
|
165
|
-
"ulimit",
|
|
166
|
-
"umask",
|
|
167
|
-
"unalias",
|
|
168
|
-
"unset",
|
|
169
|
-
"until",
|
|
170
|
-
"wait",
|
|
171
|
-
"while",
|
|
172
|
-
# Common Unix commands
|
|
173
|
-
"ls",
|
|
174
|
-
"cp",
|
|
175
|
-
"mv",
|
|
176
|
-
"rm",
|
|
177
|
-
"mkdir",
|
|
178
|
-
"rmdir",
|
|
179
|
-
"touch",
|
|
180
|
-
"cat",
|
|
181
|
-
"less",
|
|
182
|
-
"more",
|
|
183
|
-
"head",
|
|
184
|
-
"tail",
|
|
185
|
-
"grep",
|
|
186
|
-
"find",
|
|
187
|
-
"sed",
|
|
188
|
-
"awk",
|
|
189
|
-
"sort",
|
|
190
|
-
"uniq",
|
|
191
|
-
"wc",
|
|
192
|
-
"diff",
|
|
193
|
-
"chmod",
|
|
194
|
-
"chown",
|
|
195
|
-
"chgrp",
|
|
196
|
-
"ln",
|
|
197
|
-
"tar",
|
|
198
|
-
"gzip",
|
|
199
|
-
"gunzip",
|
|
200
|
-
"zip",
|
|
201
|
-
"unzip",
|
|
202
|
-
"ssh",
|
|
203
|
-
"scp",
|
|
204
|
-
"rsync",
|
|
205
|
-
"wget",
|
|
206
|
-
"curl",
|
|
207
|
-
"ping",
|
|
208
|
-
"netstat",
|
|
209
|
-
"ifconfig",
|
|
210
|
-
"route",
|
|
211
|
-
"traceroute",
|
|
212
|
-
"ps",
|
|
213
|
-
"top",
|
|
214
|
-
"htop",
|
|
215
|
-
"kill",
|
|
216
|
-
"killall",
|
|
217
|
-
"su",
|
|
218
|
-
"sudo",
|
|
219
|
-
"whoami",
|
|
220
|
-
"who",
|
|
221
|
-
"w",
|
|
222
|
-
"last",
|
|
223
|
-
"finger",
|
|
224
|
-
"uptime",
|
|
225
|
-
"free",
|
|
226
|
-
"df",
|
|
227
|
-
"du",
|
|
228
|
-
"mount",
|
|
229
|
-
"umount",
|
|
230
|
-
"fdisk",
|
|
231
|
-
"mkfs",
|
|
232
|
-
"fsck",
|
|
233
|
-
"dd",
|
|
234
|
-
"cron",
|
|
235
|
-
"at",
|
|
236
|
-
"systemctl",
|
|
237
|
-
"service",
|
|
238
|
-
"journalctl",
|
|
239
|
-
"man",
|
|
240
|
-
"info",
|
|
241
|
-
"whatis",
|
|
242
|
-
"whereis",
|
|
243
|
-
"which",
|
|
244
|
-
"date",
|
|
245
|
-
"cal",
|
|
246
|
-
"bc",
|
|
247
|
-
"expr",
|
|
248
|
-
"screen",
|
|
249
|
-
"tmux",
|
|
250
|
-
"git",
|
|
251
|
-
"vim",
|
|
252
|
-
"emacs",
|
|
253
|
-
"nano",
|
|
254
|
-
"pip",
|
|
255
|
-
]
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
def preprocess_code_block(code_text):
|
|
259
|
-
"""
|
|
260
|
-
Preprocess code block text to remove leading spaces.
|
|
261
|
-
"""
|
|
262
|
-
lines = code_text.split("\n")
|
|
263
|
-
return "\n".join(line.lstrip() for line in lines)
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
def render_code_block(code_text):
|
|
267
|
-
"""
|
|
268
|
-
Render code block with no leading spaces.
|
|
269
|
-
"""
|
|
270
|
-
processed_code = preprocess_code_block(code_text)
|
|
271
|
-
console = Console()
|
|
272
|
-
console.print(processed_code, style="")
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
def preprocess_markdown(md_text):
|
|
276
|
-
"""
|
|
277
|
-
Preprocess markdown text to handle code blocks separately.
|
|
278
|
-
"""
|
|
279
|
-
lines = md_text.split("\n")
|
|
280
|
-
processed_lines = []
|
|
281
|
-
|
|
282
|
-
inside_code_block = False
|
|
283
|
-
current_code_block = []
|
|
284
|
-
|
|
285
|
-
for line in lines:
|
|
286
|
-
if line.startswith("```"): # Toggle code block
|
|
287
|
-
if inside_code_block:
|
|
288
|
-
# Close code block, unindent, and append
|
|
289
|
-
processed_lines.append("```")
|
|
290
|
-
processed_lines.extend(
|
|
291
|
-
textwrap.dedent("\n".join(current_code_block)).split("\n")
|
|
292
|
-
)
|
|
293
|
-
processed_lines.append("```")
|
|
294
|
-
current_code_block = []
|
|
295
|
-
inside_code_block = not inside_code_block
|
|
296
|
-
elif inside_code_block:
|
|
297
|
-
current_code_block.append(line)
|
|
298
|
-
else:
|
|
299
|
-
processed_lines.append(line)
|
|
300
|
-
|
|
301
|
-
return "\n".join(processed_lines)
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
def render_markdown(text: str) -> None:
|
|
305
|
-
"""
|
|
306
|
-
Renders markdown text, but handles code blocks as plain syntax-highlighted text.
|
|
307
|
-
"""
|
|
308
|
-
lines = text.split("\n")
|
|
309
|
-
console = Console()
|
|
310
|
-
|
|
311
|
-
inside_code_block = False
|
|
312
|
-
code_lines = []
|
|
313
|
-
lang = None
|
|
314
|
-
|
|
315
|
-
for line in lines:
|
|
316
|
-
if line.startswith("```"):
|
|
317
|
-
if inside_code_block:
|
|
318
|
-
# End of code block - render the collected code
|
|
319
|
-
code = "\n".join(code_lines)
|
|
320
|
-
if code.strip():
|
|
321
|
-
syntax = Syntax(
|
|
322
|
-
code, lang or "python", theme="monokai", line_numbers=False
|
|
323
|
-
)
|
|
324
|
-
console.print(syntax)
|
|
325
|
-
code_lines = []
|
|
326
|
-
else:
|
|
327
|
-
# Start of code block - get language if specified
|
|
328
|
-
lang = line[3:].strip() or None
|
|
329
|
-
inside_code_block = not inside_code_block
|
|
330
|
-
elif inside_code_block:
|
|
331
|
-
code_lines.append(line)
|
|
332
|
-
else:
|
|
333
|
-
# Regular markdown
|
|
334
|
-
console.print(Markdown(line))
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
def render_code_block(code: str, language: str = None) -> None:
|
|
338
|
-
"""Render a code block with syntax highlighting using rich, left-justified with no line numbers"""
|
|
339
|
-
from rich.syntax import Syntax
|
|
340
|
-
from rich.console import Console
|
|
341
|
-
|
|
342
|
-
console = Console(highlight=True)
|
|
343
|
-
code = code.strip()
|
|
344
|
-
# If code starts with a language identifier, remove it
|
|
345
|
-
if code.split("\n", 1)[0].lower() in ["python", "bash", "javascript"]:
|
|
346
|
-
code = code.split("\n", 1)[1]
|
|
347
|
-
syntax = Syntax(
|
|
348
|
-
code, language or "python", theme="monokai", line_numbers=False, padding=0
|
|
349
|
-
)
|
|
350
|
-
console.print(syntax)
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
def change_directory(command_parts: list, messages: list) -> dict:
|
|
354
|
-
"""
|
|
355
|
-
Function Description:
|
|
356
|
-
Changes the current directory.
|
|
357
|
-
Args:
|
|
358
|
-
command_parts : list : Command parts
|
|
359
|
-
messages : list : Messages
|
|
360
|
-
Keyword Args:
|
|
361
|
-
None
|
|
362
|
-
Returns:
|
|
363
|
-
dict : dict : Dictionary
|
|
364
|
-
|
|
365
|
-
"""
|
|
366
|
-
|
|
367
|
-
try:
|
|
368
|
-
if len(command_parts) > 1:
|
|
369
|
-
new_dir = os.path.expanduser(command_parts[1])
|
|
370
|
-
else:
|
|
371
|
-
new_dir = os.path.expanduser("~")
|
|
372
|
-
os.chdir(new_dir)
|
|
373
|
-
return {
|
|
374
|
-
"messages": messages,
|
|
375
|
-
"output": f"Changed directory to {os.getcwd()}",
|
|
376
|
-
}
|
|
377
|
-
except FileNotFoundError:
|
|
378
|
-
return {
|
|
379
|
-
"messages": messages,
|
|
380
|
-
"output": f"Directory not found: {new_dir}",
|
|
381
|
-
}
|
|
382
|
-
except PermissionError:
|
|
383
|
-
return {"messages": messages, "output": f"Permission denied: {new_dir}"}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
def log_action(action: str, detail: str = "") -> None:
|
|
387
|
-
"""
|
|
388
|
-
Function Description:
|
|
389
|
-
This function logs an action with optional detail.
|
|
390
|
-
Args:
|
|
391
|
-
action: The action to log.
|
|
392
|
-
detail: Additional detail to log.
|
|
393
|
-
Keyword Args:
|
|
394
|
-
None
|
|
395
|
-
Returns:
|
|
396
|
-
None
|
|
397
|
-
"""
|
|
398
|
-
logging.info(f"{action}: {detail}")
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
TERMINAL_EDITORS = ["vim", "emacs", "nano"]
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
def complete(text: str, state: int) -> str:
|
|
405
|
-
"""
|
|
406
|
-
Function Description:
|
|
407
|
-
Handles autocompletion for the npcsh shell.
|
|
408
|
-
Args:
|
|
409
|
-
text : str : Text to autocomplete
|
|
410
|
-
state : int : State
|
|
411
|
-
Keyword Args:
|
|
412
|
-
None
|
|
413
|
-
Returns:
|
|
414
|
-
None
|
|
415
|
-
|
|
416
|
-
"""
|
|
417
|
-
buffer = readline.get_line_buffer()
|
|
418
|
-
available_chat_models, available_reasoning_models = get_available_models()
|
|
419
|
-
available_models = available_chat_models + available_reasoning_models
|
|
420
|
-
|
|
421
|
-
# If completing a model name
|
|
422
|
-
if "@" in buffer:
|
|
423
|
-
at_index = buffer.rfind("@")
|
|
424
|
-
model_text = buffer[at_index + 1 :]
|
|
425
|
-
model_completions = [m for m in available_models if m.startswith(model_text)]
|
|
426
|
-
|
|
427
|
-
try:
|
|
428
|
-
# Return the full text including @ symbol
|
|
429
|
-
return "@" + model_completions[state]
|
|
430
|
-
except IndexError:
|
|
431
|
-
return None
|
|
432
|
-
|
|
433
|
-
# If completing a command
|
|
434
|
-
elif text.startswith("/"):
|
|
435
|
-
command_completions = [c for c in valid_commands if c.startswith(text)]
|
|
436
|
-
try:
|
|
437
|
-
return command_completions[state]
|
|
438
|
-
except IndexError:
|
|
439
|
-
return None
|
|
440
|
-
|
|
441
|
-
return None
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
def global_completions(text: str, command_parts: list) -> list:
|
|
445
|
-
"""
|
|
446
|
-
Function Description:
|
|
447
|
-
Handles global autocompletions for the npcsh shell.
|
|
448
|
-
Args:
|
|
449
|
-
text : str : Text to autocomplete
|
|
450
|
-
command_parts : list : List of command parts
|
|
451
|
-
Keyword Args:
|
|
452
|
-
None
|
|
453
|
-
Returns:
|
|
454
|
-
completions : list : List of completions
|
|
455
|
-
|
|
456
|
-
"""
|
|
457
|
-
if not command_parts:
|
|
458
|
-
return [c + " " for c in valid_commands if c.startswith(text)]
|
|
459
|
-
elif command_parts[0] in ["/compile", "/com"]:
|
|
460
|
-
# Autocomplete NPC files
|
|
461
|
-
return [f for f in os.listdir(".") if f.endswith(".npc") and f.startswith(text)]
|
|
462
|
-
elif command_parts[0] == "/read":
|
|
463
|
-
# Autocomplete filenames
|
|
464
|
-
return [f for f in os.listdir(".") if f.startswith(text)]
|
|
465
|
-
else:
|
|
466
|
-
# Default filename completion
|
|
467
|
-
return [f for f in os.listdir(".") if f.startswith(text)]
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
def wrap_text(text: str, width: int = 80) -> str:
|
|
471
|
-
"""
|
|
472
|
-
Function Description:
|
|
473
|
-
Wraps text to a specified width.
|
|
474
|
-
Args:
|
|
475
|
-
text : str : Text to wrap
|
|
476
|
-
width : int : Width of text
|
|
477
|
-
Keyword Args:
|
|
478
|
-
None
|
|
479
|
-
Returns:
|
|
480
|
-
lines : str : Wrapped text
|
|
481
|
-
"""
|
|
482
|
-
lines = []
|
|
483
|
-
for paragraph in text.split("\n"):
|
|
484
|
-
lines.extend(textwrap.wrap(paragraph, width=width))
|
|
485
|
-
return "\n".join(lines)
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
def get_file_color(filepath: str) -> tuple:
|
|
489
|
-
"""
|
|
490
|
-
Function Description:
|
|
491
|
-
Returns color and attributes for a given file path.
|
|
492
|
-
Args:
|
|
493
|
-
filepath : str : File path
|
|
494
|
-
Keyword Args:
|
|
495
|
-
None
|
|
496
|
-
Returns:
|
|
497
|
-
color : str : Color
|
|
498
|
-
attrs : list : List of attributes
|
|
499
|
-
|
|
500
|
-
"""
|
|
501
|
-
|
|
502
|
-
if os.path.isdir(filepath):
|
|
503
|
-
return "blue", ["bold"]
|
|
504
|
-
elif os.access(filepath, os.X_OK):
|
|
505
|
-
return "green", []
|
|
506
|
-
elif filepath.endswith((".zip", ".tar", ".gz", ".bz2", ".xz", ".7z")):
|
|
507
|
-
return "red", []
|
|
508
|
-
elif filepath.endswith((".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff")):
|
|
509
|
-
return "magenta", []
|
|
510
|
-
elif filepath.endswith((".py", ".pyw")):
|
|
511
|
-
return "yellow", []
|
|
512
|
-
elif filepath.endswith((".sh", ".bash", ".zsh")):
|
|
513
|
-
return "green", []
|
|
514
|
-
elif filepath.endswith((".c", ".cpp", ".h", ".hpp")):
|
|
515
|
-
return "cyan", []
|
|
516
|
-
elif filepath.endswith((".js", ".ts", ".jsx", ".tsx")):
|
|
517
|
-
return "yellow", []
|
|
518
|
-
elif filepath.endswith((".html", ".css", ".scss", ".sass")):
|
|
519
|
-
return "magenta", []
|
|
520
|
-
elif filepath.endswith((".md", ".txt", ".log")):
|
|
521
|
-
return "white", []
|
|
522
|
-
elif filepath.startswith("."):
|
|
523
|
-
return "cyan", []
|
|
524
|
-
else:
|
|
525
|
-
return "white", []
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
def readline_safe_prompt(prompt: str) -> str:
|
|
529
|
-
"""
|
|
530
|
-
Function Description:
|
|
531
|
-
Escapes ANSI escape sequences in the prompt.
|
|
532
|
-
Args:
|
|
533
|
-
prompt : str : Prompt
|
|
534
|
-
Keyword Args:
|
|
535
|
-
None
|
|
536
|
-
Returns:
|
|
537
|
-
prompt : str : Prompt
|
|
538
|
-
|
|
539
|
-
"""
|
|
540
|
-
# This regex matches ANSI escape sequences
|
|
541
|
-
ansi_escape = re.compile(r"(\033\[[0-9;]*[a-zA-Z])")
|
|
542
|
-
|
|
543
|
-
# Wrap them with \001 and \002
|
|
544
|
-
def escape_sequence(m):
|
|
545
|
-
return "\001" + m.group(1) + "\002"
|
|
546
|
-
|
|
547
|
-
return ansi_escape.sub(escape_sequence, prompt)
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
def setup_readline() -> str:
|
|
551
|
-
"""
|
|
552
|
-
Function Description:
|
|
553
|
-
Sets up readline for the npcsh shell.
|
|
554
|
-
Args:
|
|
555
|
-
None
|
|
556
|
-
Keyword Args:
|
|
557
|
-
None
|
|
558
|
-
Returns:
|
|
559
|
-
history_file : str : History file
|
|
560
|
-
"""
|
|
561
|
-
history_file = os.path.expanduser("~/.npcsh_history")
|
|
562
|
-
try:
|
|
563
|
-
readline.read_history_file(history_file)
|
|
564
|
-
except FileNotFoundError:
|
|
565
|
-
pass
|
|
566
|
-
|
|
567
|
-
readline.set_history_length(1000)
|
|
568
|
-
readline.parse_and_bind("set enable-bracketed-paste on") # Enable paste mode
|
|
569
|
-
readline.parse_and_bind('"\e[A": history-search-backward')
|
|
570
|
-
readline.parse_and_bind('"\e[B": history-search-forward')
|
|
571
|
-
readline.parse_and_bind('"\C-r": reverse-search-history')
|
|
572
|
-
readline.parse_and_bind("\C-e: end-of-line")
|
|
573
|
-
readline.parse_and_bind("\C-a: beginning-of-line")
|
|
574
|
-
|
|
575
|
-
return history_file
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
def save_readline_history():
|
|
579
|
-
readline.write_history_file(os.path.expanduser("~/.npcsh_readline_history"))
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
def orange(text: str) -> str:
|
|
583
|
-
"""
|
|
584
|
-
Function Description:
|
|
585
|
-
Returns orange text.
|
|
586
|
-
Args:
|
|
587
|
-
text : str : Text
|
|
588
|
-
Keyword Args:
|
|
589
|
-
None
|
|
590
|
-
Returns:
|
|
591
|
-
text : str : Text
|
|
592
|
-
|
|
593
|
-
"""
|
|
594
|
-
return f"\033[38;2;255;165;0m{text}{Style.RESET_ALL}"
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
def get_multiline_input(prompt: str) -> str:
|
|
598
|
-
"""
|
|
599
|
-
Function Description:
|
|
600
|
-
Gets multiline input from the user.
|
|
601
|
-
Args:
|
|
602
|
-
prompt : str : Prompt
|
|
603
|
-
Keyword Args:
|
|
604
|
-
None
|
|
605
|
-
Returns:
|
|
606
|
-
lines : str : Lines
|
|
607
|
-
|
|
608
|
-
"""
|
|
609
|
-
lines = []
|
|
610
|
-
current_prompt = prompt
|
|
611
|
-
while True:
|
|
612
|
-
try:
|
|
613
|
-
line = input(current_prompt)
|
|
614
|
-
except EOFError:
|
|
615
|
-
print("Goodbye!")
|
|
616
|
-
break
|
|
617
|
-
|
|
618
|
-
if line.endswith("\\"):
|
|
619
|
-
lines.append(line[:-1]) # Remove the backslash
|
|
620
|
-
# Use a continuation prompt for the next line
|
|
621
|
-
current_prompt = readline_safe_prompt("> ")
|
|
622
|
-
else:
|
|
623
|
-
lines.append(line)
|
|
624
|
-
break
|
|
625
|
-
|
|
626
|
-
return "\n".join(lines)
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
def start_interactive_session(command: list) -> int:
|
|
630
|
-
"""
|
|
631
|
-
Function Description:
|
|
632
|
-
Starts an interactive session.
|
|
633
|
-
Args:
|
|
634
|
-
command : list : Command to execute
|
|
635
|
-
Keyword Args:
|
|
636
|
-
None
|
|
637
|
-
Returns:
|
|
638
|
-
returncode : int : Return code
|
|
639
|
-
|
|
640
|
-
"""
|
|
641
|
-
# Save the current terminal settings
|
|
642
|
-
old_tty = termios.tcgetattr(sys.stdin)
|
|
643
|
-
try:
|
|
644
|
-
# Create a pseudo-terminal
|
|
645
|
-
master_fd, slave_fd = pty.openpty()
|
|
646
|
-
|
|
647
|
-
# Start the process
|
|
648
|
-
p = subprocess.Popen(
|
|
649
|
-
command,
|
|
650
|
-
stdin=slave_fd,
|
|
651
|
-
stdout=slave_fd,
|
|
652
|
-
stderr=slave_fd,
|
|
653
|
-
shell=True,
|
|
654
|
-
preexec_fn=os.setsid, # Create a new process group
|
|
655
|
-
)
|
|
656
|
-
|
|
657
|
-
# Set the terminal to raw mode
|
|
658
|
-
tty.setraw(sys.stdin.fileno())
|
|
659
|
-
|
|
660
|
-
def handle_timeout(signum, frame):
|
|
661
|
-
raise TimeoutError("Process did not terminate in time")
|
|
662
|
-
|
|
663
|
-
while p.poll() is None:
|
|
664
|
-
r, w, e = select.select([sys.stdin, master_fd], [], [], 0.1)
|
|
665
|
-
if sys.stdin in r:
|
|
666
|
-
d = os.read(sys.stdin.fileno(), 10240)
|
|
667
|
-
os.write(master_fd, d)
|
|
668
|
-
elif master_fd in r:
|
|
669
|
-
o = os.read(master_fd, 10240)
|
|
670
|
-
if o:
|
|
671
|
-
os.write(sys.stdout.fileno(), o)
|
|
672
|
-
else:
|
|
673
|
-
break
|
|
674
|
-
|
|
675
|
-
# Wait for the process to terminate with a timeout
|
|
676
|
-
signal.signal(signal.SIGALRM, handle_timeout)
|
|
677
|
-
signal.alarm(5) # 5 second timeout
|
|
678
|
-
try:
|
|
679
|
-
p.wait()
|
|
680
|
-
except TimeoutError:
|
|
681
|
-
print("\nProcess did not terminate. Force killing...")
|
|
682
|
-
os.killpg(os.getpgid(p.pid), signal.SIGTERM)
|
|
683
|
-
time.sleep(1)
|
|
684
|
-
if p.poll() is None:
|
|
685
|
-
os.killpg(os.getpgid(p.pid), signal.SIGKILL)
|
|
686
|
-
finally:
|
|
687
|
-
signal.alarm(0)
|
|
688
|
-
|
|
689
|
-
finally:
|
|
690
|
-
# Restore the terminal settings
|
|
691
|
-
termios.tcsetattr(sys.stdin, termios.TCSAFLUSH, old_tty)
|
|
692
|
-
|
|
693
|
-
return p.returncode
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
def validate_bash_command(command_parts: list) -> bool:
|
|
697
|
-
"""
|
|
698
|
-
Function Description:
|
|
699
|
-
Validate if the command sequence is a valid bash command with proper arguments/flags.
|
|
700
|
-
Args:
|
|
701
|
-
command_parts : list : Command parts
|
|
702
|
-
Keyword Args:
|
|
703
|
-
None
|
|
704
|
-
Returns:
|
|
705
|
-
bool : bool : Boolean
|
|
706
|
-
"""
|
|
707
|
-
if not command_parts:
|
|
708
|
-
return False
|
|
709
|
-
|
|
710
|
-
COMMAND_PATTERNS = {
|
|
711
|
-
"cat": {
|
|
712
|
-
"flags": ["-n", "-b", "-E", "-T", "-s", "--number", "-A", "--show-all"],
|
|
713
|
-
"requires_arg": True,
|
|
714
|
-
},
|
|
715
|
-
"find": {
|
|
716
|
-
"flags": [
|
|
717
|
-
"-name",
|
|
718
|
-
"-type",
|
|
719
|
-
"-size",
|
|
720
|
-
"-mtime",
|
|
721
|
-
"-exec",
|
|
722
|
-
"-print",
|
|
723
|
-
"-delete",
|
|
724
|
-
"-maxdepth",
|
|
725
|
-
"-mindepth",
|
|
726
|
-
"-perm",
|
|
727
|
-
"-user",
|
|
728
|
-
"-group",
|
|
729
|
-
],
|
|
730
|
-
"requires_arg": True,
|
|
731
|
-
},
|
|
732
|
-
"who": {
|
|
733
|
-
"flags": [
|
|
734
|
-
"-a",
|
|
735
|
-
"-b",
|
|
736
|
-
"-d",
|
|
737
|
-
"-H",
|
|
738
|
-
"-l",
|
|
739
|
-
"-p",
|
|
740
|
-
"-q",
|
|
741
|
-
"-r",
|
|
742
|
-
"-s",
|
|
743
|
-
"-t",
|
|
744
|
-
"-u",
|
|
745
|
-
"--all",
|
|
746
|
-
"--count",
|
|
747
|
-
"--heading",
|
|
748
|
-
],
|
|
749
|
-
"requires_arg": True,
|
|
750
|
-
},
|
|
751
|
-
"open": {
|
|
752
|
-
"flags": ["-a", "-e", "-t", "-f", "-F", "-W", "-n", "-g", "-h"],
|
|
753
|
-
"requires_arg": True,
|
|
754
|
-
},
|
|
755
|
-
"which": {"flags": ["-a", "-s", "-v"], "requires_arg": True},
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
base_command = command_parts[0]
|
|
759
|
-
|
|
760
|
-
if base_command not in COMMAND_PATTERNS:
|
|
761
|
-
return True # Allow other commands to pass through
|
|
762
|
-
|
|
763
|
-
pattern = COMMAND_PATTERNS[base_command]
|
|
764
|
-
args = []
|
|
765
|
-
flags = []
|
|
766
|
-
|
|
767
|
-
for i in range(1, len(command_parts)):
|
|
768
|
-
part = command_parts[i]
|
|
769
|
-
if part.startswith("-"):
|
|
770
|
-
flags.append(part)
|
|
771
|
-
if part not in pattern["flags"]:
|
|
772
|
-
return False # Invalid flag
|
|
773
|
-
else:
|
|
774
|
-
args.append(part)
|
|
775
|
-
|
|
776
|
-
# Check if 'who' has any arguments (it shouldn't)
|
|
777
|
-
if base_command == "who" and args:
|
|
778
|
-
return False
|
|
779
|
-
|
|
780
|
-
# Handle 'which' with '-a' flag
|
|
781
|
-
if base_command == "which" and "-a" in flags:
|
|
782
|
-
return True # Allow 'which -a' with or without arguments.
|
|
783
|
-
|
|
784
|
-
# Check if any required arguments are missing
|
|
785
|
-
if pattern.get("requires_arg", False) and not args:
|
|
786
|
-
return False
|
|
787
|
-
|
|
788
|
-
return True
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
def execute_squish_command():
|
|
792
|
-
return
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
def execute_splat_command():
|
|
796
|
-
return
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
def execute_rag_command(
|
|
800
|
-
command: str,
|
|
801
|
-
messages=None,
|
|
802
|
-
) -> dict:
|
|
803
|
-
"""
|
|
804
|
-
Execute the RAG command with support for embedding generation using
|
|
805
|
-
nomic-embed-text.
|
|
806
|
-
"""
|
|
807
|
-
|
|
808
|
-
if messages is None:
|
|
809
|
-
messages = []
|
|
810
|
-
|
|
811
|
-
parts = command.split()
|
|
812
|
-
search_terms = []
|
|
813
|
-
params = {}
|
|
814
|
-
file_list = []
|
|
815
|
-
|
|
816
|
-
# Parse command parts
|
|
817
|
-
for i, part in enumerate(parts):
|
|
818
|
-
if "=" in part: # This is a parameter
|
|
819
|
-
key, value = part.split("=", 1)
|
|
820
|
-
params[key.strip()] = value.strip()
|
|
821
|
-
elif part.startswith("-f"): # Handle the file list
|
|
822
|
-
if i + 1 < len(parts):
|
|
823
|
-
wildcard_pattern = parts[i + 1]
|
|
824
|
-
file_list.extend(glob.glob(wildcard_pattern))
|
|
825
|
-
else: # This is part of the search term
|
|
826
|
-
search_terms.append(part)
|
|
827
|
-
|
|
828
|
-
# print(params)
|
|
829
|
-
# -top_k will also be a flaggable param
|
|
830
|
-
if "-top_k" in params:
|
|
831
|
-
top_k = int(params["-top_k"])
|
|
832
|
-
else:
|
|
833
|
-
top_k = 5
|
|
834
|
-
|
|
835
|
-
# If no files found, inform the user
|
|
836
|
-
if not file_list:
|
|
837
|
-
return {
|
|
838
|
-
"messages": messages,
|
|
839
|
-
"output": "No files found matching the specified pattern.",
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
search_term = " ".join(search_terms)
|
|
843
|
-
docs_to_embed = []
|
|
844
|
-
|
|
845
|
-
# try:
|
|
846
|
-
# Load each file and generate embeddings
|
|
847
|
-
for filename in file_list:
|
|
848
|
-
extension = os.path.splitext(filename)[1].lower()
|
|
849
|
-
if os.path.exists(filename):
|
|
850
|
-
if extension in [
|
|
851
|
-
".txt",
|
|
852
|
-
".csv",
|
|
853
|
-
".yaml",
|
|
854
|
-
".json",
|
|
855
|
-
".md",
|
|
856
|
-
".r",
|
|
857
|
-
".c",
|
|
858
|
-
".java",
|
|
859
|
-
".cpp",
|
|
860
|
-
".h",
|
|
861
|
-
".hpp",
|
|
862
|
-
".xlsx",
|
|
863
|
-
".py",
|
|
864
|
-
".js",
|
|
865
|
-
".ts",
|
|
866
|
-
".html",
|
|
867
|
-
".css",
|
|
868
|
-
".ipynb",
|
|
869
|
-
".pdf",
|
|
870
|
-
".docx",
|
|
871
|
-
".pptx",
|
|
872
|
-
".ppt",
|
|
873
|
-
".npc",
|
|
874
|
-
".tool",
|
|
875
|
-
".doc",
|
|
876
|
-
".xls",
|
|
877
|
-
]:
|
|
878
|
-
if extension == ".csv":
|
|
879
|
-
df = pd.read_csv(filename)
|
|
880
|
-
file_texts = df.apply(
|
|
881
|
-
lambda row: " ".join(row.values.astype(str)), axis=1
|
|
882
|
-
).tolist()
|
|
883
|
-
else:
|
|
884
|
-
with open(filename, "r", encoding="utf-8") as file:
|
|
885
|
-
file_texts = file.readlines()
|
|
886
|
-
file_texts = [
|
|
887
|
-
line.strip() for line in file_texts if line.strip() != ""
|
|
888
|
-
]
|
|
889
|
-
docs_to_embed.extend(file_texts)
|
|
890
|
-
else:
|
|
891
|
-
return {
|
|
892
|
-
"messages": messages,
|
|
893
|
-
"output": f"Unsupported file type: {extension} for file {filename}",
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
similar_texts = search_similar_texts(
|
|
897
|
-
search_term,
|
|
898
|
-
docs_to_embed=docs_to_embed,
|
|
899
|
-
top_k=top_k, # Adjust as necessary
|
|
900
|
-
)
|
|
901
|
-
|
|
902
|
-
# Format results
|
|
903
|
-
output = "Found similar texts:\n\n"
|
|
904
|
-
if similar_texts:
|
|
905
|
-
for result in similar_texts:
|
|
906
|
-
output += f"Score: {result['score']:.3f}\n"
|
|
907
|
-
output += f"Text: {result['text']}\n"
|
|
908
|
-
if "id" in result:
|
|
909
|
-
output += f"ID: {result['id']}\n"
|
|
910
|
-
output += "\n"
|
|
911
|
-
else:
|
|
912
|
-
output = "No similar texts found in the database."
|
|
913
|
-
|
|
914
|
-
# Additional information about processed files
|
|
915
|
-
output += "\nProcessed Files:\n"
|
|
916
|
-
output += "\n".join(file_list)
|
|
917
|
-
|
|
918
|
-
return {"messages": messages, "output": output}
|
|
919
|
-
|
|
920
|
-
# except Exception as e:
|
|
921
|
-
# return {
|
|
922
|
-
# "messages": messages,
|
|
923
|
-
# "output": f"Error during RAG search: {str(e)}",
|
|
924
|
-
# }
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
def filter_by_date(
|
|
928
|
-
similar_texts: List[dict], start_date: str, end_date: str
|
|
929
|
-
) -> List[dict]:
|
|
930
|
-
"""
|
|
931
|
-
Filter the similar texts based on start and end dates.
|
|
932
|
-
Args:
|
|
933
|
-
similar_texts (List[dict]): The similar texts to filter.
|
|
934
|
-
start_date (str): The start date in 'YYYY-MM-DD' format.
|
|
935
|
-
end_date (str): The end date in 'YYYY-MM-DD' format.
|
|
936
|
-
|
|
937
|
-
Returns:
|
|
938
|
-
List[dict]: Filtered similar texts.
|
|
939
|
-
"""
|
|
940
|
-
filtered_results = []
|
|
941
|
-
start = datetime.strptime(start_date, "%Y-%m-%d")
|
|
942
|
-
end = datetime.strptime(end_date, "%Y-%m-%d")
|
|
943
|
-
|
|
944
|
-
for text in similar_texts:
|
|
945
|
-
text_date = datetime.strptime(
|
|
946
|
-
text["date"], "%Y-%m-%d"
|
|
947
|
-
) # Assuming 'date' is an attribut
|
|
948
|
-
|
|
949
|
-
if start <= text_date <= end:
|
|
950
|
-
filtered_results.append(text)
|
|
951
|
-
|
|
952
|
-
return filtered_results
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
def execute_search_command(
|
|
956
|
-
command: str,
|
|
957
|
-
messages=None,
|
|
958
|
-
provider: str = None,
|
|
959
|
-
):
|
|
960
|
-
"""
|
|
961
|
-
Function Description:
|
|
962
|
-
Executes a search command.
|
|
963
|
-
Args:
|
|
964
|
-
command : str : Command
|
|
965
|
-
db_path : str : Database path
|
|
966
|
-
npc_compiler : NPCCompiler : NPC compiler
|
|
967
|
-
Keyword Args:
|
|
968
|
-
embedding_model : None : Embedding model
|
|
969
|
-
current_npc : None : Current NPC
|
|
970
|
-
text_data : None : Text data
|
|
971
|
-
text_data_embedded : None : Embedded text data
|
|
972
|
-
messages : None : Messages
|
|
973
|
-
Returns:
|
|
974
|
-
dict : dict : Dictionary
|
|
975
|
-
|
|
976
|
-
"""
|
|
977
|
-
# search commands will bel ike :
|
|
978
|
-
# '/search -p default = google "search term" '
|
|
979
|
-
# '/search -p perplexity ..
|
|
980
|
-
# '/search -p google ..
|
|
981
|
-
# extract provider if its there
|
|
982
|
-
# check for either -p or --p
|
|
983
|
-
|
|
984
|
-
search_command = command.split()
|
|
985
|
-
if any("-p" in s for s in search_command) or any(
|
|
986
|
-
"--provider" in s for s in search_command
|
|
987
|
-
):
|
|
988
|
-
provider = (
|
|
989
|
-
search_command[search_command.index("-p") + 1]
|
|
990
|
-
if "-p" in search_command
|
|
991
|
-
else search_command[search_command.index("--provider") + 1]
|
|
992
|
-
)
|
|
993
|
-
else:
|
|
994
|
-
provider = None
|
|
995
|
-
if any("-n" in s for s in search_command) or any(
|
|
996
|
-
"--num_results" in s for s in search_command
|
|
997
|
-
):
|
|
998
|
-
num_results = (
|
|
999
|
-
search_command[search_command.index("-n") + 1]
|
|
1000
|
-
if "-n" in search_command
|
|
1001
|
-
else search_command[search_command.index("--num_results") + 1]
|
|
1002
|
-
)
|
|
1003
|
-
else:
|
|
1004
|
-
num_results = 5
|
|
1005
|
-
|
|
1006
|
-
# remove the -p and provider from the command string
|
|
1007
|
-
command = command.replace(f"-p {provider}", "").replace(
|
|
1008
|
-
f"--provider {provider}", ""
|
|
1009
|
-
)
|
|
1010
|
-
result = search_web(command, num_results=num_results, provider=provider)
|
|
1011
|
-
if messages is None:
|
|
1012
|
-
messages = []
|
|
1013
|
-
messages.append({"role": "user", "content": command})
|
|
1014
|
-
|
|
1015
|
-
messages.append(
|
|
1016
|
-
{"role": "assistant", "content": result[0] + f" \n Citation Links: {result[1]}"}
|
|
1017
|
-
)
|
|
1018
|
-
|
|
1019
|
-
return {
|
|
1020
|
-
"messages": messages,
|
|
1021
|
-
"output": result[0] + f"\n\n\n Citation Links: {result[1]}",
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
def extract_tool_inputs(args: List[str], tool: Tool) -> Dict[str, Any]:
|
|
1026
|
-
inputs = {}
|
|
1027
|
-
|
|
1028
|
-
# Create flag mapping
|
|
1029
|
-
flag_mapping = {}
|
|
1030
|
-
for input_ in tool.inputs:
|
|
1031
|
-
if isinstance(input_, str):
|
|
1032
|
-
flag_mapping[f"-{input_[0]}"] = input_
|
|
1033
|
-
flag_mapping[f"--{input_}"] = input_
|
|
1034
|
-
elif isinstance(input_, dict):
|
|
1035
|
-
key = list(input_.keys())[0]
|
|
1036
|
-
flag_mapping[f"-{key[0]}"] = key
|
|
1037
|
-
flag_mapping[f"--{key}"] = key
|
|
1038
|
-
|
|
1039
|
-
# Process arguments
|
|
1040
|
-
used_args = set()
|
|
1041
|
-
for i, arg in enumerate(args):
|
|
1042
|
-
if arg in flag_mapping:
|
|
1043
|
-
# If flag is found, next argument is its value
|
|
1044
|
-
if i + 1 < len(args):
|
|
1045
|
-
input_name = flag_mapping[arg]
|
|
1046
|
-
inputs[input_name] = args[i + 1]
|
|
1047
|
-
used_args.add(i)
|
|
1048
|
-
used_args.add(i + 1)
|
|
1049
|
-
else:
|
|
1050
|
-
print(f"Warning: {arg} flag is missing a value.")
|
|
1051
|
-
|
|
1052
|
-
# If no flags used, combine remaining args for first input
|
|
1053
|
-
unused_args = [arg for i, arg in enumerate(args) if i not in used_args]
|
|
1054
|
-
if unused_args and tool.inputs:
|
|
1055
|
-
first_input = tool.inputs[0]
|
|
1056
|
-
if isinstance(first_input, str):
|
|
1057
|
-
inputs[first_input] = " ".join(unused_args)
|
|
1058
|
-
elif isinstance(first_input, dict):
|
|
1059
|
-
key = list(first_input.keys())[0]
|
|
1060
|
-
inputs[key] = " ".join(unused_args)
|
|
1061
|
-
|
|
1062
|
-
# Add default values for inputs not provided
|
|
1063
|
-
for input_ in tool.inputs:
|
|
1064
|
-
if isinstance(input_, str):
|
|
1065
|
-
if input_ not in inputs:
|
|
1066
|
-
if any(args): # If we have any arguments at all
|
|
1067
|
-
raise ValueError(f"Missing required input: {input_}")
|
|
1068
|
-
else:
|
|
1069
|
-
inputs[input_] = None # Allow None for completely empty calls
|
|
1070
|
-
elif isinstance(input_, dict):
|
|
1071
|
-
key = list(input_.keys())[0]
|
|
1072
|
-
if key not in inputs:
|
|
1073
|
-
inputs[key] = input_[key]
|
|
1074
|
-
|
|
1075
|
-
return inputs
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
import math
|
|
1079
|
-
from PIL import Image
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
def resize_image_tars(image_path):
|
|
1083
|
-
image = Image.open(image_path)
|
|
1084
|
-
max_pixels = 6000 * 28 * 28
|
|
1085
|
-
if image.width * image.height > max_pixels:
|
|
1086
|
-
max_pixels = 2700 * 28 * 28
|
|
1087
|
-
else:
|
|
1088
|
-
max_pixels = 1340 * 28 * 28
|
|
1089
|
-
resize_factor = math.sqrt(max_pixels / (image.width * image.height))
|
|
1090
|
-
width, height = int(image.width * resize_factor), int(image.height * resize_factor)
|
|
1091
|
-
image = image.resize((width, height))
|
|
1092
|
-
image.save(image_path, format="png")
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
def execute_plan_command(
|
|
1096
|
-
command, npc=None, model=None, provider=None, messages=None, api_url=None
|
|
1097
|
-
):
|
|
1098
|
-
parts = command.split(maxsplit=1)
|
|
1099
|
-
if len(parts) < 2:
|
|
1100
|
-
return {
|
|
1101
|
-
"messages": messages,
|
|
1102
|
-
"output": "Usage: /plan <command and schedule description>",
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
request = parts[1]
|
|
1106
|
-
platform_system = platform.system()
|
|
1107
|
-
|
|
1108
|
-
# Create standard directories
|
|
1109
|
-
jobs_dir = os.path.expanduser("~/.npcsh/jobs")
|
|
1110
|
-
logs_dir = os.path.expanduser("~/.npcsh/logs")
|
|
1111
|
-
os.makedirs(jobs_dir, exist_ok=True)
|
|
1112
|
-
os.makedirs(logs_dir, exist_ok=True)
|
|
1113
|
-
|
|
1114
|
-
# First part - just the request formatting
|
|
1115
|
-
linux_request = f"""Convert this scheduling request into a crontab-based script:
|
|
1116
|
-
Request: {request}
|
|
1117
|
-
|
|
1118
|
-
"""
|
|
1119
|
-
|
|
1120
|
-
# Second part - the static prompt with examples and requirements
|
|
1121
|
-
linux_prompt_static = """Example for "record CPU usage every 10 minutes":
|
|
1122
|
-
{
|
|
1123
|
-
"script": "#!/bin/bash
|
|
1124
|
-
set -euo pipefail
|
|
1125
|
-
IFS=$'\\n\\t'
|
|
1126
|
-
|
|
1127
|
-
LOGFILE=\"$HOME/.npcsh/logs/cpu_usage.log\"
|
|
1128
|
-
|
|
1129
|
-
log_info() {
|
|
1130
|
-
echo \"[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] $*\" >> \"$LOGFILE\"
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
log_error() {
|
|
1134
|
-
echo \"[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $*\" >> \"$LOGFILE\"
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
record_cpu() {
|
|
1138
|
-
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
|
1139
|
-
local cpu_usage=$(top -bn1 | grep 'Cpu(s)' | awk '{print $2}')
|
|
1140
|
-
log_info \"CPU Usage: $cpu_usage%\"
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
record_cpu",
|
|
1144
|
-
"schedule": "*/10 * * * *",
|
|
1145
|
-
"description": "Record CPU usage every 10 minutes",
|
|
1146
|
-
"name": "record_cpu_usage"
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
Your response must be valid json with the following keys:
|
|
1150
|
-
- script: The shell script content with proper functions and error handling. special characters must be escaped to ensure python json.loads will work correctly.
|
|
1151
|
-
- schedule: Crontab expression (5 fields: minute hour day month weekday)
|
|
1152
|
-
- description: A human readable description
|
|
1153
|
-
- name: A unique name for the job
|
|
1154
|
-
|
|
1155
|
-
Do not include any additional markdown formatting in your response or leading ```json tags."""
|
|
1156
|
-
|
|
1157
|
-
mac_request = f"""Convert this scheduling request into a launchd-compatible script:
|
|
1158
|
-
Request: {request}
|
|
1159
|
-
|
|
1160
|
-
"""
|
|
1161
|
-
|
|
1162
|
-
mac_prompt_static = """Example for "record CPU usage every 10 minutes":
|
|
1163
|
-
{
|
|
1164
|
-
"script": "#!/bin/bash
|
|
1165
|
-
set -euo pipefail
|
|
1166
|
-
IFS=$'\\n\\t'
|
|
1167
|
-
|
|
1168
|
-
LOGFILE=\"$HOME/.npcsh/logs/cpu_usage.log\"
|
|
1169
|
-
|
|
1170
|
-
log_info() {
|
|
1171
|
-
echo \"[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] $*\" >> \"$LOGFILE\"
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
log_error() {
|
|
1175
|
-
echo \"[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $*\" >> \"$LOGFILE\"
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
record_cpu() {
|
|
1179
|
-
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
|
1180
|
-
local cpu_usage=$(top -l 1 | grep 'CPU usage' | awk '{print $3}' | tr -d '%')
|
|
1181
|
-
log_info \"CPU Usage: $cpu_usage%\"
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
record_cpu",
|
|
1185
|
-
"schedule": "600",
|
|
1186
|
-
"description": "Record CPU usage every 10 minutes",
|
|
1187
|
-
"name": "record_cpu_usage"
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
Your response must be valid json with the following keys:
|
|
1191
|
-
- script: The shell script content with proper functions and error handling. special characters must be escaped to ensure python json.loads will work correctly.
|
|
1192
|
-
- schedule: Interval in seconds (e.g. 600 for 10 minutes)
|
|
1193
|
-
- description: A human readable description
|
|
1194
|
-
- name: A unique name for the job
|
|
1195
|
-
|
|
1196
|
-
Do not include any additional markdown formatting in your response or leading ```json tags."""
|
|
1197
|
-
|
|
1198
|
-
windows_request = f"""Convert this scheduling request into a PowerShell script with Task Scheduler parameters:
|
|
1199
|
-
Request: {request}
|
|
1200
|
-
|
|
1201
|
-
"""
|
|
1202
|
-
|
|
1203
|
-
windows_prompt_static = """Example for "record CPU usage every 10 minutes":
|
|
1204
|
-
{
|
|
1205
|
-
"script": "$ErrorActionPreference = 'Stop'
|
|
1206
|
-
|
|
1207
|
-
$LogFile = \"$HOME\\.npcsh\\logs\\cpu_usage.log\"
|
|
1208
|
-
|
|
1209
|
-
function Write-Log {
|
|
1210
|
-
param($Message, $Type = 'INFO')
|
|
1211
|
-
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
|
|
1212
|
-
\"[$timestamp] [$Type] $Message\" | Out-File -FilePath $LogFile -Append
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
function Get-CpuUsage {
|
|
1216
|
-
try {
|
|
1217
|
-
$cpu = (Get-Counter '\\Processor(_Total)\\% Processor Time').CounterSamples.CookedValue
|
|
1218
|
-
Write-Log \"CPU Usage: $($cpu)%\"
|
|
1219
|
-
} catch {
|
|
1220
|
-
Write-Log $_.Exception.Message 'ERROR'
|
|
1221
|
-
throw
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
|
-
Get-CpuUsage",
|
|
1226
|
-
"schedule": "/sc minute /mo 10",
|
|
1227
|
-
"description": "Record CPU usage every 10 minutes",
|
|
1228
|
-
"name": "record_cpu_usage"
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
Your response must be valid json with the following keys:
|
|
1232
|
-
- script: The PowerShell script content with proper functions and error handling. special characters must be escaped to ensure python json.loads will work correctly.
|
|
1233
|
-
- schedule: Task Scheduler parameters (e.g. /sc minute /mo 10)
|
|
1234
|
-
- description: A human readable description
|
|
1235
|
-
- name: A unique name for the job
|
|
1236
|
-
|
|
1237
|
-
Do not include any additional markdown formatting in your response or leading ```json tags."""
|
|
1238
|
-
|
|
1239
|
-
prompts = {
|
|
1240
|
-
"Linux": linux_request + linux_prompt_static,
|
|
1241
|
-
"Darwin": mac_request + mac_prompt_static,
|
|
1242
|
-
"Windows": windows_request + windows_prompt_static,
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
prompt = prompts[platform_system]
|
|
1246
|
-
response = get_llm_response(
|
|
1247
|
-
prompt, npc=npc, model=model, provider=provider, format="json"
|
|
1248
|
-
)
|
|
1249
|
-
schedule_info = response.get("response")
|
|
1250
|
-
print("Received schedule info:", schedule_info)
|
|
1251
|
-
|
|
1252
|
-
job_name = f"job_{schedule_info['name']}"
|
|
1253
|
-
|
|
1254
|
-
if platform_system == "Windows":
|
|
1255
|
-
script_path = os.path.join(jobs_dir, f"{job_name}.ps1")
|
|
1256
|
-
else:
|
|
1257
|
-
script_path = os.path.join(jobs_dir, f"{job_name}.sh")
|
|
1258
|
-
|
|
1259
|
-
log_path = os.path.join(logs_dir, f"{job_name}.log")
|
|
1260
|
-
|
|
1261
|
-
# Write the script
|
|
1262
|
-
with open(script_path, "w") as f:
|
|
1263
|
-
f.write(schedule_info["script"])
|
|
1264
|
-
os.chmod(script_path, 0o755)
|
|
1265
|
-
|
|
1266
|
-
if platform_system == "Linux":
|
|
1267
|
-
try:
|
|
1268
|
-
current_crontab = subprocess.check_output(["crontab", "-l"], text=True)
|
|
1269
|
-
except subprocess.CalledProcessError:
|
|
1270
|
-
current_crontab = ""
|
|
1271
|
-
|
|
1272
|
-
crontab_line = f"{schedule_info['schedule']} {script_path} >> {log_path} 2>&1"
|
|
1273
|
-
new_crontab = current_crontab.strip() + "\n" + crontab_line + "\n"
|
|
1274
|
-
|
|
1275
|
-
with tempfile.NamedTemporaryFile(mode="w") as tmp:
|
|
1276
|
-
tmp.write(new_crontab)
|
|
1277
|
-
tmp.flush()
|
|
1278
|
-
subprocess.run(["crontab", tmp.name], check=True)
|
|
1279
|
-
|
|
1280
|
-
output = f"""Job created successfully:
|
|
1281
|
-
- Description: {schedule_info['description']}
|
|
1282
|
-
- Schedule: {schedule_info['schedule']}
|
|
1283
|
-
- Script: {script_path}
|
|
1284
|
-
- Log: {log_path}
|
|
1285
|
-
- Crontab entry: {crontab_line}"""
|
|
1286
|
-
|
|
1287
|
-
elif platform_system == "Darwin":
|
|
1288
|
-
plist_dir = os.path.expanduser("~/Library/LaunchAgents")
|
|
1289
|
-
os.makedirs(plist_dir, exist_ok=True)
|
|
1290
|
-
plist_path = os.path.join(plist_dir, f"com.npcsh.{job_name}.plist")
|
|
1291
|
-
|
|
1292
|
-
plist_content = f"""<?xml version="1.0" encoding="UTF-8"?>
|
|
1293
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1294
|
-
<plist version="1.0">
|
|
1295
|
-
<dict>
|
|
1296
|
-
<key>Label</key>
|
|
1297
|
-
<string>com.npcsh.{job_name}</string>
|
|
1298
|
-
<key>ProgramArguments</key>
|
|
1299
|
-
<array>
|
|
1300
|
-
<string>{script_path}</string>
|
|
1301
|
-
</array>
|
|
1302
|
-
<key>StartInterval</key>
|
|
1303
|
-
<integer>{schedule_info['schedule']}</integer>
|
|
1304
|
-
<key>StandardOutPath</key>
|
|
1305
|
-
<string>{log_path}</string>
|
|
1306
|
-
<key>StandardErrorPath</key>
|
|
1307
|
-
<string>{log_path}</string>
|
|
1308
|
-
<key>RunAtLoad</key>
|
|
1309
|
-
<true/>
|
|
1310
|
-
</dict>
|
|
1311
|
-
</plist>"""
|
|
1312
|
-
|
|
1313
|
-
with open(plist_path, "w") as f:
|
|
1314
|
-
f.write(plist_content)
|
|
1315
|
-
|
|
1316
|
-
subprocess.run(["launchctl", "unload", plist_path], check=False)
|
|
1317
|
-
subprocess.run(["launchctl", "load", plist_path], check=True)
|
|
1318
|
-
|
|
1319
|
-
output = f"""Job created successfully:
|
|
1320
|
-
- Description: {schedule_info['description']}
|
|
1321
|
-
- Schedule: Every {schedule_info['schedule']} seconds
|
|
1322
|
-
- Script: {script_path}
|
|
1323
|
-
- Log: {log_path}
|
|
1324
|
-
- Launchd plist: {plist_path}"""
|
|
1325
|
-
|
|
1326
|
-
elif platform_system == "Windows":
|
|
1327
|
-
task_name = f"NPCSH_{job_name}"
|
|
1328
|
-
|
|
1329
|
-
# Parse schedule_info['schedule'] into individual parameters
|
|
1330
|
-
schedule_params = schedule_info["schedule"].split()
|
|
1331
|
-
|
|
1332
|
-
cmd = (
|
|
1333
|
-
[
|
|
1334
|
-
"schtasks",
|
|
1335
|
-
"/create",
|
|
1336
|
-
"/tn",
|
|
1337
|
-
task_name,
|
|
1338
|
-
"/tr",
|
|
1339
|
-
f"powershell -NoProfile -ExecutionPolicy Bypass -File {script_path}",
|
|
1340
|
-
]
|
|
1341
|
-
+ schedule_params
|
|
1342
|
-
+ ["/f"]
|
|
1343
|
-
) # /f forces creation if task exists
|
|
1344
|
-
|
|
1345
|
-
subprocess.run(cmd, check=True)
|
|
1346
|
-
|
|
1347
|
-
output = f"""Job created successfully:
|
|
1348
|
-
- Description: {schedule_info['description']}
|
|
1349
|
-
- Schedule: {schedule_info['schedule']}
|
|
1350
|
-
- Script: {script_path}
|
|
1351
|
-
- Log: {log_path}
|
|
1352
|
-
- Task name: {task_name}"""
|
|
1353
|
-
|
|
1354
|
-
return {"messages": messages, "output": output}
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
def execute_trigger_command(
|
|
1358
|
-
command, npc=None, model=None, provider=None, messages=None, api_url=None
|
|
1359
|
-
):
|
|
1360
|
-
parts = command.split(maxsplit=1)
|
|
1361
|
-
if len(parts) < 2:
|
|
1362
|
-
return {
|
|
1363
|
-
"messages": messages,
|
|
1364
|
-
"output": "Usage: /trigger <trigger condition and action description>",
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
request = parts[1]
|
|
1368
|
-
platform_system = platform.system()
|
|
1369
|
-
|
|
1370
|
-
linux_request = f"""Convert this trigger request into a single event-monitoring daemon script:
|
|
1371
|
-
Request: {request}
|
|
1372
|
-
|
|
1373
|
-
"""
|
|
1374
|
-
|
|
1375
|
-
linux_prompt_static = """Example for "Move PDFs from Downloads to Documents/PDFs":
|
|
1376
|
-
{
|
|
1377
|
-
"script": "#!/bin/bash\\nset -euo pipefail\\nIFS=$'\\n\\t'\\n\\nLOGFILE=\\\"$HOME/.npcsh/logs/pdf_mover.log\\\"\\nSOURCE=\\\"$HOME/Downloads\\\"\\nTARGET=\\\"$HOME/Documents/PDFs\\\"\\n\\nlog_info() {\\n echo \\\"[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] $*\\\" >> \\\"$LOGFILE\\\"\\n}\\n\\nlog_error() {\\n echo \\\"[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $*\\\" >> \\\"$LOGFILE\\\"\\n}\\n\\ninotifywait -m -q -e create --format '%w%f' \\\"$SOURCE\\\" | while read filepath; do\\n if [[ \\\"$filepath\\\" =~ \\\\.pdf$ ]]; then\\n mv \\\"$filepath\\\" \\\"$TARGET/\\\" && log_info \\\"Moved $filepath to $TARGET\\\" || log_error \\\"Failed to move $filepath\\\"\\n fi\\ndone",
|
|
1378
|
-
"name": "pdf_mover",
|
|
1379
|
-
"description": "Move PDF files from Downloads to Documents/PDFs folder"
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
|
-
The script MUST:
|
|
1383
|
-
- Use inotifywait -m -q -e create --format '%w%f' to get full paths
|
|
1384
|
-
- Double quote ALL file operations: "$SOURCE/$FILE"
|
|
1385
|
-
- Use $HOME for absolute paths
|
|
1386
|
-
- Echo both success and failure messages to log
|
|
1387
|
-
|
|
1388
|
-
Your response must be valid json with the following keys:
|
|
1389
|
-
- script: The shell script content with proper functions and error handling
|
|
1390
|
-
- name: A unique name for the trigger
|
|
1391
|
-
- description: A human readable description
|
|
1392
|
-
|
|
1393
|
-
Do not include any additional markdown formatting in your response."""
|
|
1394
|
-
|
|
1395
|
-
mac_request = f"""Convert this trigger request into a single event-monitoring daemon script:
|
|
1396
|
-
Request: {request}
|
|
1397
|
-
|
|
1398
|
-
"""
|
|
1399
|
-
|
|
1400
|
-
mac_prompt_static = """Example for "Move PDFs from Downloads to Documents/PDFs":
|
|
1401
|
-
{
|
|
1402
|
-
"script": "#!/bin/bash\\nset -euo pipefail\\nIFS=$'\\n\\t'\\n\\nLOGFILE=\\\"$HOME/.npcsh/logs/pdf_mover.log\\\"\\nSOURCE=\\\"$HOME/Downloads\\\"\\nTARGET=\\\"$HOME/Documents/PDFs\\\"\\n\\nlog_info() {\\n echo \\\"[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] $*\\\" >> \\\"$LOGFILE\\\"\\n}\\n\\nlog_error() {\\n echo \\\"[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $*\\\" >> \\\"$LOGFILE\\\"\\n}\\n\\nfswatch -0 -r -e '.*' --event Created --format '%p' \\\"$SOURCE\\\" | while read -d '' filepath; do\\n if [[ \\\"$filepath\\\" =~ \\\\.pdf$ ]]; then\\n mv \\\"$filepath\\\" \\\"$TARGET/\\\" && log_info \\\"Moved $filepath to $TARGET\\\" || log_error \\\"Failed to move $filepath\\\"\\n fi\\ndone",
|
|
1403
|
-
"name": "pdf_mover",
|
|
1404
|
-
"description": "Move PDF files from Downloads to Documents/PDFs folder"
|
|
1405
|
-
}
|
|
1406
|
-
|
|
1407
|
-
The script MUST:
|
|
1408
|
-
- Use fswatch -0 -r -e '.*' --event Created --format '%p' to get full paths
|
|
1409
|
-
- Double quote ALL file operations: "$SOURCE/$FILE"
|
|
1410
|
-
- Use $HOME for absolute paths
|
|
1411
|
-
- Echo both success and failure messages to log
|
|
1412
|
-
|
|
1413
|
-
Your response must be valid json with the following keys:
|
|
1414
|
-
- script: The shell script content with proper functions and error handling
|
|
1415
|
-
- name: A unique name for the trigger
|
|
1416
|
-
- description: A human readable description
|
|
1417
|
-
|
|
1418
|
-
Do not include any additional markdown formatting in your response."""
|
|
1419
|
-
|
|
1420
|
-
windows_request = f"""Convert this trigger request into a single event-monitoring daemon script:
|
|
1421
|
-
Request: {request}
|
|
1422
|
-
|
|
1423
|
-
"""
|
|
1424
|
-
|
|
1425
|
-
windows_prompt_static = """Example for "Move PDFs from Downloads to Documents/PDFs":
|
|
1426
|
-
{
|
|
1427
|
-
"script": "$ErrorActionPreference = 'Stop'\\n\\n$LogFile = \\\"$HOME\\.npcsh\\logs\\pdf_mover.log\\\"\\n$Source = \\\"$HOME\\Downloads\\\"\\n$Target = \\\"$HOME\\Documents\\PDFs\\\"\\n\\nfunction Write-Log {\\n param($Message, $Type = 'INFO')\\n $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'\\n \\\"[$timestamp] [$Type] $Message\\\" | Out-File -FilePath $LogFile -Append\\n}\\n\\n$watcher = New-Object System.IO.FileSystemWatcher\\n$watcher.Path = $Source\\n$watcher.Filter = \\\"*.pdf\\\"\\n$watcher.IncludeSubdirectories = $true\\n$watcher.EnableRaisingEvents = $true\\n\\n$action = {\\n $path = $Event.SourceEventArgs.FullPath\\n try {\\n Move-Item -Path $path -Destination $Target\\n Write-Log \\\"Moved $path to $Target\\\"\\n } catch {\\n Write-Log $_.Exception.Message 'ERROR'\\n }\\n}\\n\\nRegister-ObjectEvent $watcher 'Created' -Action $action\\n\\nwhile ($true) { Start-Sleep 1 }",
|
|
1428
|
-
"name": "pdf_mover",
|
|
1429
|
-
"description": "Move PDF files from Downloads to Documents/PDFs folder"
|
|
1430
|
-
}
|
|
1431
|
-
|
|
1432
|
-
The script MUST:
|
|
1433
|
-
- Use FileSystemWatcher for monitoring
|
|
1434
|
-
- Double quote ALL file operations: "$Source\\$File"
|
|
1435
|
-
- Use $HOME for absolute paths
|
|
1436
|
-
- Echo both success and failure messages to log
|
|
1437
|
-
|
|
1438
|
-
Your response must be valid json with the following keys:
|
|
1439
|
-
- script: The PowerShell script content with proper functions and error handling
|
|
1440
|
-
- name: A unique name for the trigger
|
|
1441
|
-
- description: A human readable description
|
|
1442
|
-
|
|
1443
|
-
Do not include any additional markdown formatting in your response."""
|
|
1444
|
-
|
|
1445
|
-
prompts = {
|
|
1446
|
-
"Linux": linux_request + linux_prompt_static,
|
|
1447
|
-
"Darwin": mac_request + mac_prompt_static,
|
|
1448
|
-
"Windows": windows_request + windows_prompt_static,
|
|
1449
|
-
}
|
|
1450
|
-
|
|
1451
|
-
prompt = prompts[platform_system]
|
|
1452
|
-
response = get_llm_response(
|
|
1453
|
-
prompt, npc=npc, model=model, provider=provider, format="json"
|
|
1454
|
-
)
|
|
1455
|
-
trigger_info = response.get("response")
|
|
1456
|
-
print("Trigger info:", trigger_info)
|
|
1457
|
-
|
|
1458
|
-
triggers_dir = os.path.expanduser("~/.npcsh/triggers")
|
|
1459
|
-
logs_dir = os.path.expanduser("~/.npcsh/logs")
|
|
1460
|
-
os.makedirs(triggers_dir, exist_ok=True)
|
|
1461
|
-
os.makedirs(logs_dir, exist_ok=True)
|
|
1462
|
-
|
|
1463
|
-
trigger_name = f"trigger_{trigger_info['name']}"
|
|
1464
|
-
log_path = os.path.join(logs_dir, f"{trigger_name}.log")
|
|
1465
|
-
|
|
1466
|
-
if platform_system == "Linux":
|
|
1467
|
-
script_path = os.path.join(triggers_dir, f"{trigger_name}.sh")
|
|
1468
|
-
|
|
1469
|
-
with open(script_path, "w") as f:
|
|
1470
|
-
f.write(trigger_info["script"])
|
|
1471
|
-
os.chmod(script_path, 0o755)
|
|
1472
|
-
|
|
1473
|
-
service_dir = os.path.expanduser("~/.config/systemd/user")
|
|
1474
|
-
os.makedirs(service_dir, exist_ok=True)
|
|
1475
|
-
service_path = os.path.join(service_dir, f"npcsh-{trigger_name}.service")
|
|
1476
|
-
|
|
1477
|
-
service_content = f"""[Unit]
|
|
1478
|
-
Description={trigger_info['description']}
|
|
1479
|
-
After=network.target
|
|
1480
|
-
|
|
1481
|
-
[Service]
|
|
1482
|
-
Type=simple
|
|
1483
|
-
ExecStart={script_path}
|
|
1484
|
-
Restart=always
|
|
1485
|
-
StandardOutput=append:{log_path}
|
|
1486
|
-
StandardError=append:{log_path}
|
|
1487
|
-
|
|
1488
|
-
[Install]
|
|
1489
|
-
WantedBy=default.target
|
|
1490
|
-
"""
|
|
1491
|
-
|
|
1492
|
-
with open(service_path, "w") as f:
|
|
1493
|
-
f.write(service_content)
|
|
1494
|
-
|
|
1495
|
-
subprocess.run(["systemctl", "--user", "daemon-reload"])
|
|
1496
|
-
subprocess.run(
|
|
1497
|
-
["systemctl", "--user", "enable", f"npcsh-{trigger_name}.service"]
|
|
1498
|
-
)
|
|
1499
|
-
subprocess.run(
|
|
1500
|
-
["systemctl", "--user", "start", f"npcsh-{trigger_name}.service"]
|
|
1501
|
-
)
|
|
1502
|
-
|
|
1503
|
-
status = subprocess.run(
|
|
1504
|
-
["systemctl", "--user", "status", f"npcsh-{trigger_name}.service"],
|
|
1505
|
-
capture_output=True,
|
|
1506
|
-
text=True,
|
|
1507
|
-
)
|
|
1508
|
-
|
|
1509
|
-
output = f"""Trigger service created:
|
|
1510
|
-
- Description: {trigger_info['description']}
|
|
1511
|
-
- Script: {script_path}
|
|
1512
|
-
- Service: {service_path}
|
|
1513
|
-
- Log: {log_path}
|
|
1514
|
-
|
|
1515
|
-
Status:
|
|
1516
|
-
{status.stdout}"""
|
|
1517
|
-
|
|
1518
|
-
elif platform_system == "Darwin":
|
|
1519
|
-
script_path = os.path.join(triggers_dir, f"{trigger_name}.sh")
|
|
1520
|
-
|
|
1521
|
-
with open(script_path, "w") as f:
|
|
1522
|
-
f.write(trigger_info["script"])
|
|
1523
|
-
os.chmod(script_path, 0o755)
|
|
1524
|
-
|
|
1525
|
-
plist_dir = os.path.expanduser("~/Library/LaunchAgents")
|
|
1526
|
-
os.makedirs(plist_dir, exist_ok=True)
|
|
1527
|
-
plist_path = os.path.join(plist_dir, f"com.npcsh.{trigger_name}.plist")
|
|
1528
|
-
|
|
1529
|
-
plist_content = f"""<?xml version="1.0" encoding="UTF-8"?>
|
|
1530
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1531
|
-
<plist version="1.0">
|
|
1532
|
-
<dict>
|
|
1533
|
-
<key>Label</key>
|
|
1534
|
-
<string>com.npcsh.{trigger_name}</string>
|
|
1535
|
-
<key>ProgramArguments</key>
|
|
1536
|
-
<array>
|
|
1537
|
-
<string>{script_path}</string>
|
|
1538
|
-
</array>
|
|
1539
|
-
<key>RunAtLoad</key>
|
|
1540
|
-
<true/>
|
|
1541
|
-
<key>KeepAlive</key>
|
|
1542
|
-
<true/>
|
|
1543
|
-
<key>StandardOutPath</key>
|
|
1544
|
-
<string>{log_path}</string>
|
|
1545
|
-
<key>StandardErrorPath</key>
|
|
1546
|
-
<string>{log_path}</string>
|
|
1547
|
-
</dict>
|
|
1548
|
-
</plist>"""
|
|
1549
|
-
|
|
1550
|
-
with open(plist_path, "w") as f:
|
|
1551
|
-
f.write(plist_content)
|
|
1552
|
-
|
|
1553
|
-
subprocess.run(["launchctl", "unload", plist_path], check=False)
|
|
1554
|
-
subprocess.run(["launchctl", "load", plist_path], check=True)
|
|
1555
|
-
|
|
1556
|
-
output = f"""Trigger service created:
|
|
1557
|
-
- Description: {trigger_info['description']}
|
|
1558
|
-
- Script: {script_path}
|
|
1559
|
-
- Launchd plist: {plist_path}
|
|
1560
|
-
- Log: {log_path}"""
|
|
1561
|
-
|
|
1562
|
-
elif platform_system == "Windows":
|
|
1563
|
-
script_path = os.path.join(triggers_dir, f"{trigger_name}.ps1")
|
|
1564
|
-
|
|
1565
|
-
with open(script_path, "w") as f:
|
|
1566
|
-
f.write(trigger_info["script"])
|
|
1567
|
-
|
|
1568
|
-
task_name = f"NPCSH_{trigger_name}"
|
|
1569
|
-
|
|
1570
|
-
# Create a scheduled task that runs at startup
|
|
1571
|
-
cmd = [
|
|
1572
|
-
"schtasks",
|
|
1573
|
-
"/create",
|
|
1574
|
-
"/tn",
|
|
1575
|
-
task_name,
|
|
1576
|
-
"/tr",
|
|
1577
|
-
f"powershell -NoProfile -ExecutionPolicy Bypass -File {script_path}",
|
|
1578
|
-
"/sc",
|
|
1579
|
-
"onstart",
|
|
1580
|
-
"/ru",
|
|
1581
|
-
"System",
|
|
1582
|
-
"/f", # Force creation
|
|
1583
|
-
]
|
|
1584
|
-
|
|
1585
|
-
subprocess.run(cmd, check=True)
|
|
1586
|
-
|
|
1587
|
-
# Start the task immediately
|
|
1588
|
-
subprocess.run(["schtasks", "/run", "/tn", task_name])
|
|
1589
|
-
|
|
1590
|
-
output = f"""Trigger service created:
|
|
1591
|
-
- Description: {trigger_info['description']}
|
|
1592
|
-
- Script: {script_path}
|
|
1593
|
-
- Task name: {task_name}
|
|
1594
|
-
- Log: {log_path}"""
|
|
1595
|
-
|
|
1596
|
-
return {"messages": messages, "output": output}
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
def enter_wander_mode(args, messages, npc_compiler, npc, model, provider):
|
|
1600
|
-
"""
|
|
1601
|
-
Wander mode is an exploratory mode where an LLM is given a task and they begin to wander through space.
|
|
1602
|
-
As they wander, they drift in between conscious thought and popcorn-like subconscious thought
|
|
1603
|
-
The former is triggered by external stimuli andw when these stimuli come we will capture the recent high entropy
|
|
1604
|
-
infromation from the subconscious popcorn thoughts and then consider them with respect to the initial problem at hand.
|
|
1605
|
-
|
|
1606
|
-
The conscious evaluator will attempt to connect them, thus functionalizing the verse-jumping algorithm
|
|
1607
|
-
outlined by Everything Everywhere All at Once.
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
"""
|
|
1611
|
-
return
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
def ots(
|
|
1615
|
-
command_parts,
|
|
1616
|
-
npc=None,
|
|
1617
|
-
model: str = NPCSH_VISION_MODEL,
|
|
1618
|
-
provider: str = NPCSH_VISION_PROVIDER,
|
|
1619
|
-
api_url: str = NPCSH_API_URL,
|
|
1620
|
-
api_key: str = None,
|
|
1621
|
-
):
|
|
1622
|
-
# check if there is a filename
|
|
1623
|
-
if len(command_parts) > 1:
|
|
1624
|
-
filename = command_parts[1]
|
|
1625
|
-
file_path = os.path.join(os.getcwd(), filename)
|
|
1626
|
-
# Get user prompt about the image
|
|
1627
|
-
user_prompt = input(
|
|
1628
|
-
"Enter a prompt for the LLM about this image (or press Enter to skip): "
|
|
1629
|
-
)
|
|
1630
|
-
|
|
1631
|
-
output = analyze_image(
|
|
1632
|
-
user_prompt, file_path, filename, npc=npc, model=model, provider=provider
|
|
1633
|
-
)
|
|
1634
|
-
messages = [
|
|
1635
|
-
{"role": "user", "content": user_prompt},
|
|
1636
|
-
]
|
|
1637
|
-
|
|
1638
|
-
else:
|
|
1639
|
-
output = capture_screenshot(npc=npc)
|
|
1640
|
-
user_prompt = input(
|
|
1641
|
-
"Enter a prompt for the LLM about this image (or press Enter to skip): "
|
|
1642
|
-
)
|
|
1643
|
-
# print(output["model_kwargs"])
|
|
1644
|
-
output = analyze_image(
|
|
1645
|
-
user_prompt,
|
|
1646
|
-
output["file_path"],
|
|
1647
|
-
output["filename"],
|
|
1648
|
-
npc=npc,
|
|
1649
|
-
model=model,
|
|
1650
|
-
provider=provider,
|
|
1651
|
-
api_url=api_url,
|
|
1652
|
-
api_key=api_key,
|
|
1653
|
-
)
|
|
1654
|
-
# messages = output["messages"]
|
|
1655
|
-
|
|
1656
|
-
output = output["response"]
|
|
1657
|
-
messages = [
|
|
1658
|
-
{"role": "user", "content": user_prompt},
|
|
1659
|
-
]
|
|
1660
|
-
if output:
|
|
1661
|
-
if isinstance(output, dict) and "filename" in output:
|
|
1662
|
-
message = f"Screenshot captured: {output['filename']}\nFull path: {output['file_path']}\nLLM-ready data available."
|
|
1663
|
-
else: # This handles both LLM responses and error messages (both strings)
|
|
1664
|
-
message = output
|
|
1665
|
-
messages.append({"role": "assistant", "content": message})
|
|
1666
|
-
return {"messages": messages, "output": message} # Return the message
|
|
1667
|
-
else: # Handle the case where capture_screenshot returns None
|
|
1668
|
-
print("Screenshot capture failed.")
|
|
1669
|
-
return {
|
|
1670
|
-
"messages": messages,
|
|
1671
|
-
"output": None,
|
|
1672
|
-
} # Return None to indicate failure
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
def get_help():
|
|
1676
|
-
output = """# Available Commands
|
|
1677
|
-
|
|
1678
|
-
/com [npc_file1.npc npc_file2.npc ...] # Alias for /compile.
|
|
1679
|
-
|
|
1680
|
-
/compile [npc_file1.npc npc_file2.npc ...] # Compiles specified NPC profile(s). If no arguments are provided, compiles all NPCs in the npc_profi
|
|
1681
|
-
|
|
1682
|
-
/exit or /quit # Exits the current NPC mode or the npcsh shell.
|
|
1683
|
-
|
|
1684
|
-
/help # Displays this help message.
|
|
1685
|
-
|
|
1686
|
-
/init # Initializes a new NPC project.
|
|
1687
|
-
|
|
1688
|
-
/notes # Enter notes mode.
|
|
1689
|
-
|
|
1690
|
-
/ots [filename] # Analyzes an image from a specified filename or captures a screenshot for analysis.
|
|
1691
|
-
|
|
1692
|
-
/rag <search_term> # Performs a RAG (Retrieval-Augmented Generation) search based on the search term provided.
|
|
1693
|
-
|
|
1694
|
-
/sample <question> # Asks the current NPC a question.
|
|
1695
|
-
|
|
1696
|
-
/set <model|provider|db_path> <value> # Sets the specified parameter. Enclose the value in quotes.
|
|
1697
|
-
|
|
1698
|
-
/sp [inherit_last=<n>] # Alias for /spool.
|
|
1699
|
-
|
|
1700
|
-
/spool [inherit_last=<n>] # Enters spool mode. Optionally inherits the last <n> messages.
|
|
1701
|
-
|
|
1702
|
-
/vixynt [filename=<filename>] <prompt> # Captures a screenshot and generates an image with the specified prompt.
|
|
1703
|
-
|
|
1704
|
-
/<subcommand> # Enters the specified NPC's mode.
|
|
1705
|
-
|
|
1706
|
-
/cmd <command/> # Execute a command using the current NPC's LLM.
|
|
1707
|
-
|
|
1708
|
-
/command <command/> # Alias for /cmd.
|
|
1709
|
-
|
|
1710
|
-
Tools within your npc_team directory can also be used as macro commands.
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
# Note
|
|
1714
|
-
Bash commands and other programs can be executed directly. """
|
|
1715
|
-
return output
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
def execute_tool_command(
|
|
1719
|
-
tool: Tool,
|
|
1720
|
-
args: List[str],
|
|
1721
|
-
npc_compiler: NPCCompiler,
|
|
1722
|
-
messages=None,
|
|
1723
|
-
npc: NPC = None,
|
|
1724
|
-
) -> Dict[str, Any]:
|
|
1725
|
-
"""
|
|
1726
|
-
Execute a tool command with the given arguments.
|
|
1727
|
-
"""
|
|
1728
|
-
# Extract inputs for the current tool
|
|
1729
|
-
input_values = extract_tool_inputs(args, tool)
|
|
1730
|
-
|
|
1731
|
-
# print(f"Input values: {input_values}")
|
|
1732
|
-
# Execute the tool with the extracted inputs
|
|
1733
|
-
|
|
1734
|
-
tool_output = tool.execute(
|
|
1735
|
-
input_values,
|
|
1736
|
-
npc_compiler.all_tools_dict,
|
|
1737
|
-
npc_compiler.jinja_env,
|
|
1738
|
-
tool.tool_name,
|
|
1739
|
-
npc=npc,
|
|
1740
|
-
)
|
|
1741
|
-
|
|
1742
|
-
return {"messages": messages, "output": tool_output}
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
def print_tools(tools):
|
|
1746
|
-
output = "Available tools:"
|
|
1747
|
-
for tool in tools:
|
|
1748
|
-
output += f" {tool.tool_name}"
|
|
1749
|
-
output += f" Description: {tool.description}"
|
|
1750
|
-
output += f" Inputs: {tool.inputs}"
|
|
1751
|
-
return output
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
def execute_slash_command(
|
|
1755
|
-
command: str,
|
|
1756
|
-
db_path: str,
|
|
1757
|
-
db_conn: sqlite3.Connection,
|
|
1758
|
-
npc_compiler: NPCCompiler,
|
|
1759
|
-
valid_npcs: list,
|
|
1760
|
-
npc: NPC = None,
|
|
1761
|
-
messages=None,
|
|
1762
|
-
model: str = None,
|
|
1763
|
-
provider: str = None,
|
|
1764
|
-
api_url: str = None,
|
|
1765
|
-
conversation_id: str = None,
|
|
1766
|
-
stream: bool = False,
|
|
1767
|
-
):
|
|
1768
|
-
"""
|
|
1769
|
-
Function Description:
|
|
1770
|
-
Executes a slash command.
|
|
1771
|
-
Args:
|
|
1772
|
-
command : str : Command
|
|
1773
|
-
db_path : str : Database path
|
|
1774
|
-
npc_compiler : NPCCompiler : NPC compiler
|
|
1775
|
-
Keyword Args:
|
|
1776
|
-
embedding_model : None : Embedding model
|
|
1777
|
-
current_npc : None : Current NPC
|
|
1778
|
-
text_data : None : Text data
|
|
1779
|
-
text_data_embedded : None : Embedded text data
|
|
1780
|
-
messages : None : Messages
|
|
1781
|
-
Returns:
|
|
1782
|
-
dict : dict : Dictionary
|
|
1783
|
-
"""
|
|
1784
|
-
tools = npc_compiler.all_tools
|
|
1785
|
-
|
|
1786
|
-
command = command[1:]
|
|
1787
|
-
|
|
1788
|
-
log_action("Command Executed", command)
|
|
1789
|
-
|
|
1790
|
-
command_parts = command.split()
|
|
1791
|
-
command_name = command_parts[0]
|
|
1792
|
-
args = command_parts[1:]
|
|
1793
|
-
|
|
1794
|
-
current_npc = npc
|
|
1795
|
-
if command_name in valid_npcs:
|
|
1796
|
-
npc_path = get_npc_path(command_name, db_path)
|
|
1797
|
-
current_npc = load_npc_from_file(npc_path, db_conn)
|
|
1798
|
-
output = f"Switched to NPC: {current_npc.name}"
|
|
1799
|
-
# print(output)
|
|
1800
|
-
|
|
1801
|
-
return {"messages": messages, "output": output, "current_npc": current_npc}
|
|
1802
|
-
|
|
1803
|
-
if command_name == "compile" or command_name == "com":
|
|
1804
|
-
try:
|
|
1805
|
-
if len(args) > 0: # Specific NPC file(s) provided
|
|
1806
|
-
for npc_file in args:
|
|
1807
|
-
# differentiate between .npc and .pipe
|
|
1808
|
-
if npc_file.endswith(".pipe"):
|
|
1809
|
-
# Initialize the PipelineRunner with the appropriate parameters
|
|
1810
|
-
pipeline_runner = PipelineRunner(
|
|
1811
|
-
pipeline_file=npc_file, # Uses the current NPC file
|
|
1812
|
-
db_path="~/npcsh_history.db", # Ensure this path is correctly set
|
|
1813
|
-
npc_root_dir="./npc_team", # Adjust this to your actual NPC directory
|
|
1814
|
-
)
|
|
1815
|
-
|
|
1816
|
-
# Execute the pipeline and capture the output
|
|
1817
|
-
output = pipeline_runner.execute_pipeline()
|
|
1818
|
-
|
|
1819
|
-
# Format the output if needed
|
|
1820
|
-
output = f"Compiled Pipeline: {output}\n"
|
|
1821
|
-
elif npc_file.endswith(".npc"):
|
|
1822
|
-
compiled_script = npc_compiler.compile(npc_file)
|
|
1823
|
-
|
|
1824
|
-
output = f"Compiled NPC profile: {compiled_script}\n"
|
|
1825
|
-
elif current_npc: # Compile current NPC
|
|
1826
|
-
compiled_script = npc_compiler.compile(current_npc)
|
|
1827
|
-
output = f"Compiled NPC profile: {compiled_script}"
|
|
1828
|
-
else: # Compile all NPCs in the directory
|
|
1829
|
-
output = ""
|
|
1830
|
-
for filename in os.listdir(npc_compiler.npc_directory):
|
|
1831
|
-
if filename.endswith(".npc"):
|
|
1832
|
-
try:
|
|
1833
|
-
compiled_script = npc_compiler.compile(
|
|
1834
|
-
npc_compiler.npc_directory + "/" + filename
|
|
1835
|
-
)
|
|
1836
|
-
output += (
|
|
1837
|
-
f"Compiled NPC profile: {compiled_script['name']}\n"
|
|
1838
|
-
)
|
|
1839
|
-
except Exception as e:
|
|
1840
|
-
output += f"Error compiling {filename}: {str(e)}\n"
|
|
1841
|
-
|
|
1842
|
-
except Exception as e:
|
|
1843
|
-
import traceback
|
|
1844
|
-
|
|
1845
|
-
output = f"Error compiling NPC profile: {str(e)}\n{traceback.format_exc()}"
|
|
1846
|
-
print(output)
|
|
1847
|
-
elif command_name == "tools":
|
|
1848
|
-
return {"messages": messages, "output": print_tools(tools)}
|
|
1849
|
-
elif command_name == "plan":
|
|
1850
|
-
return execute_plan_command(
|
|
1851
|
-
command,
|
|
1852
|
-
npc=npc,
|
|
1853
|
-
model=model,
|
|
1854
|
-
provider=provider,
|
|
1855
|
-
api_url=api_url,
|
|
1856
|
-
messages=messages,
|
|
1857
|
-
)
|
|
1858
|
-
elif command_name == "trigger":
|
|
1859
|
-
return execute_trigger_command(
|
|
1860
|
-
command,
|
|
1861
|
-
npc=npc,
|
|
1862
|
-
model=model,
|
|
1863
|
-
provider=provider,
|
|
1864
|
-
api_url=api_url,
|
|
1865
|
-
messages=messages,
|
|
1866
|
-
)
|
|
1867
|
-
|
|
1868
|
-
elif command_name == "plonk":
|
|
1869
|
-
request = " ".join(args)
|
|
1870
|
-
plonk_call = plonk(
|
|
1871
|
-
request, action_space, model=model, provider=provider, npc=npc
|
|
1872
|
-
)
|
|
1873
|
-
return {"messages": messages, "output": plonk_call, "current_npc": current_npc}
|
|
1874
|
-
elif command_name == "wander":
|
|
1875
|
-
return enter_wander_mode(args, messages, npc_compiler, npc, model, provider)
|
|
1876
|
-
|
|
1877
|
-
elif command_name in [tool.tool_name for tool in tools]:
|
|
1878
|
-
tool = next((tool for tool in tools if tool.tool_name == command_name), None)
|
|
1879
|
-
|
|
1880
|
-
return execute_tool_command(
|
|
1881
|
-
tool,
|
|
1882
|
-
args,
|
|
1883
|
-
npc_compiler,
|
|
1884
|
-
messages,
|
|
1885
|
-
npc=npc,
|
|
1886
|
-
)
|
|
1887
|
-
elif command_name == "flush":
|
|
1888
|
-
n = float("inf") # Default to infinite
|
|
1889
|
-
for arg in args:
|
|
1890
|
-
if arg.startswith("n="):
|
|
1891
|
-
try:
|
|
1892
|
-
n = int(arg.split("=")[1])
|
|
1893
|
-
except ValueError:
|
|
1894
|
-
return {
|
|
1895
|
-
"messages": messages,
|
|
1896
|
-
"output": "Error: 'n' must be an integer." + "\n",
|
|
1897
|
-
}
|
|
1898
|
-
|
|
1899
|
-
flush_result = flush_messages(n, messages)
|
|
1900
|
-
return flush_result # Return the result of flushing messages
|
|
1901
|
-
|
|
1902
|
-
# Handle /rehash command
|
|
1903
|
-
elif command_name == "rehash":
|
|
1904
|
-
rehash_result = rehash_last_message(
|
|
1905
|
-
conversation_id, model=model, provider=provider, npc=npc
|
|
1906
|
-
)
|
|
1907
|
-
return rehash_result # Return the result of rehashing last message
|
|
1908
|
-
|
|
1909
|
-
elif command_name == "pipe":
|
|
1910
|
-
if len(args) > 0: # Specific NPC file(s) provided
|
|
1911
|
-
for npc_file in args:
|
|
1912
|
-
# differentiate between .npc and .pipe
|
|
1913
|
-
pipeline_runner = PipelineRunner(
|
|
1914
|
-
pipeline_file=npc_file, # Uses the current NPC file
|
|
1915
|
-
db_path="~/npcsh_history.db", # Ensure this path is correctly set
|
|
1916
|
-
npc_root_dir="./npc_team", # Adjust this to your actual NPC directory
|
|
1917
|
-
)
|
|
1918
|
-
|
|
1919
|
-
# run through the steps in the pipe
|
|
1920
|
-
elif command_name == "select":
|
|
1921
|
-
query = " ".join([command_name] + args) # Reconstruct full query
|
|
1922
|
-
|
|
1923
|
-
try:
|
|
1924
|
-
with sqlite3.connect(db_path) as conn:
|
|
1925
|
-
cursor = conn.cursor()
|
|
1926
|
-
cursor.execute(query)
|
|
1927
|
-
rows = cursor.fetchall()
|
|
1928
|
-
|
|
1929
|
-
if not rows:
|
|
1930
|
-
output = "No results found"
|
|
1931
|
-
else:
|
|
1932
|
-
# Get column names
|
|
1933
|
-
columns = [description[0] for description in cursor.description]
|
|
1934
|
-
|
|
1935
|
-
# Format output as table
|
|
1936
|
-
table_lines = []
|
|
1937
|
-
table_lines.append(" | ".join(columns))
|
|
1938
|
-
table_lines.append("-" * len(table_lines[0]))
|
|
1939
|
-
|
|
1940
|
-
for row in rows:
|
|
1941
|
-
table_lines.append(" | ".join(str(col) for col in row))
|
|
1942
|
-
|
|
1943
|
-
output = "\n".join(table_lines)
|
|
1944
|
-
|
|
1945
|
-
return {"messages": messages, "output": output}
|
|
1946
|
-
|
|
1947
|
-
except sqlite3.Error as e:
|
|
1948
|
-
output = f"Database error: {str(e)}"
|
|
1949
|
-
return {"messages": messages, "output": output}
|
|
1950
|
-
elif command_name == "init":
|
|
1951
|
-
output = initialize_npc_project()
|
|
1952
|
-
return {"messages": messages, "output": output}
|
|
1953
|
-
elif (
|
|
1954
|
-
command.startswith("vixynt")
|
|
1955
|
-
or command.startswith("vix")
|
|
1956
|
-
or (command.startswith("v") and command[1] == " ")
|
|
1957
|
-
):
|
|
1958
|
-
# check if "filename=..." is in the command
|
|
1959
|
-
filename = None
|
|
1960
|
-
if "filename=" in command:
|
|
1961
|
-
filename = command.split("filename=")[1].split()[0]
|
|
1962
|
-
command = command.replace(f"filename={filename}", "").strip()
|
|
1963
|
-
# Get user prompt about the image BY joining the rest of the arguments
|
|
1964
|
-
user_prompt = " ".join(command.split()[1:])
|
|
1965
|
-
|
|
1966
|
-
output = generate_image(
|
|
1967
|
-
user_prompt, npc=npc, filename=filename, model=model, provider=provider
|
|
1968
|
-
)
|
|
1969
|
-
|
|
1970
|
-
elif command.startswith("ots"):
|
|
1971
|
-
return ots(
|
|
1972
|
-
command_parts, model=model, provider=provider, npc=npc, api_url=api_url
|
|
1973
|
-
)
|
|
1974
|
-
elif command_name == "help": # New help command
|
|
1975
|
-
return {
|
|
1976
|
-
"messages": messages,
|
|
1977
|
-
"output": get_help(),
|
|
1978
|
-
}
|
|
1979
|
-
|
|
1980
|
-
elif command_name == "whisper":
|
|
1981
|
-
# try:
|
|
1982
|
-
messages = enter_whisper_mode(npc=npc)
|
|
1983
|
-
output = "Exited whisper mode."
|
|
1984
|
-
# except Exception as e:
|
|
1985
|
-
# print(f"Error entering whisper mode: {str(e)}")
|
|
1986
|
-
# output = "Error entering whisper mode"
|
|
1987
|
-
|
|
1988
|
-
elif command_name == "notes":
|
|
1989
|
-
output = enter_notes_mode(npc=npc)
|
|
1990
|
-
elif command_name == "data":
|
|
1991
|
-
# print("data")
|
|
1992
|
-
output = enter_data_mode(npc=npc)
|
|
1993
|
-
# output = enter_observation_mode(, npc=npc)
|
|
1994
|
-
elif command_name == "cmd" or command_name == "command":
|
|
1995
|
-
output = execute_llm_command(
|
|
1996
|
-
command,
|
|
1997
|
-
npc=npc,
|
|
1998
|
-
stream=stream,
|
|
1999
|
-
messages=messages,
|
|
2000
|
-
)
|
|
2001
|
-
|
|
2002
|
-
elif command_name == "rag":
|
|
2003
|
-
output = execute_rag_command(command, messages=messages)
|
|
2004
|
-
messages = output["messages"]
|
|
2005
|
-
output = output["output"]
|
|
2006
|
-
|
|
2007
|
-
elif command_name == "set":
|
|
2008
|
-
parts = command.split()
|
|
2009
|
-
if len(parts) == 3 and parts[1] in ["model", "provider", "db_path"]:
|
|
2010
|
-
output = execute_set_command(parts[1], parts[2])
|
|
2011
|
-
else:
|
|
2012
|
-
return {
|
|
2013
|
-
"messages": messages,
|
|
2014
|
-
"output": "Invalid set command. Usage: /set [model|provider|db_path] 'value_in_quotes' ",
|
|
2015
|
-
}
|
|
2016
|
-
elif command_name == "search":
|
|
2017
|
-
output = execute_search_command(
|
|
2018
|
-
command,
|
|
2019
|
-
messages=messages,
|
|
2020
|
-
)
|
|
2021
|
-
messages = output["messages"]
|
|
2022
|
-
# print(output, type(output))
|
|
2023
|
-
output = output["output"]
|
|
2024
|
-
# print(output, type(output))
|
|
2025
|
-
elif command_name == "sample":
|
|
2026
|
-
output = execute_llm_question(
|
|
2027
|
-
" ".join(command.split()[1:]), # Skip the command name
|
|
2028
|
-
npc=npc,
|
|
2029
|
-
messages=[],
|
|
2030
|
-
model=model,
|
|
2031
|
-
provider=provider,
|
|
2032
|
-
stream=stream,
|
|
2033
|
-
)
|
|
2034
|
-
elif command_name == "spool" or command_name == "sp":
|
|
2035
|
-
inherit_last = 0
|
|
2036
|
-
device = "cpu"
|
|
2037
|
-
rag_similarity_threshold = 0.3
|
|
2038
|
-
for part in args:
|
|
2039
|
-
if part.startswith("inherit_last="):
|
|
2040
|
-
try:
|
|
2041
|
-
inherit_last = int(part.split("=")[1])
|
|
2042
|
-
except ValueError:
|
|
2043
|
-
return {
|
|
2044
|
-
"messages": messages,
|
|
2045
|
-
"output": "Error: inherit_last must be an integer",
|
|
2046
|
-
}
|
|
2047
|
-
if part.startswith("device="):
|
|
2048
|
-
device = part.split("=")[1]
|
|
2049
|
-
if part.startswith("rag_similarity_threshold="):
|
|
2050
|
-
rag_similarity_threshold = float(part.split("=")[1])
|
|
2051
|
-
if part.startswith("model="):
|
|
2052
|
-
model = part.split("=")[1]
|
|
2053
|
-
|
|
2054
|
-
if part.startswith("provider="):
|
|
2055
|
-
provider = part.split("=")[1]
|
|
2056
|
-
if part.startswith("api_url="):
|
|
2057
|
-
api_url = part.split("=")[1]
|
|
2058
|
-
if part.startswith("api_key="):
|
|
2059
|
-
api_key = part.split("=")[1]
|
|
2060
|
-
|
|
2061
|
-
# load the npc properly
|
|
2062
|
-
|
|
2063
|
-
match = re.search(r"files=\s*\[(.*?)\]", command)
|
|
2064
|
-
files = []
|
|
2065
|
-
if match:
|
|
2066
|
-
# Extract file list from the command
|
|
2067
|
-
files = [
|
|
2068
|
-
file.strip().strip("'").strip('"') for file in match.group(1).split(",")
|
|
2069
|
-
]
|
|
2070
|
-
|
|
2071
|
-
# Call the enter_spool_mode with the list of files
|
|
2072
|
-
else:
|
|
2073
|
-
files = None
|
|
2074
|
-
|
|
2075
|
-
if len(command_parts) >= 2 and command_parts[1] == "reattach":
|
|
2076
|
-
last_conversation = command_history.get_last_conversation_by_path(
|
|
2077
|
-
os.getcwd()
|
|
2078
|
-
)
|
|
2079
|
-
print(last_conversation)
|
|
2080
|
-
if last_conversation:
|
|
2081
|
-
spool_context = [
|
|
2082
|
-
{"role": part[2], "content": part[3]} for part in last_conversation
|
|
2083
|
-
]
|
|
2084
|
-
|
|
2085
|
-
print(f"Reattached to previous conversation:\n\n")
|
|
2086
|
-
output = enter_spool_mode(
|
|
2087
|
-
inherit_last,
|
|
2088
|
-
files=files,
|
|
2089
|
-
npc=npc,
|
|
2090
|
-
model=model,
|
|
2091
|
-
provider=provider,
|
|
2092
|
-
rag_similarity_threshold=rag_similarity_threshold,
|
|
2093
|
-
device=device,
|
|
2094
|
-
messages=spool_context,
|
|
2095
|
-
conversation_id=conversation_id,
|
|
2096
|
-
stream=stream,
|
|
2097
|
-
)
|
|
2098
|
-
return {"messages": output["messages"], "output": output}
|
|
2099
|
-
|
|
2100
|
-
else:
|
|
2101
|
-
return {"messages": [], "output": "No previous conversation found."}
|
|
2102
|
-
|
|
2103
|
-
output = enter_spool_mode(
|
|
2104
|
-
inherit_last,
|
|
2105
|
-
files=files,
|
|
2106
|
-
npc=npc,
|
|
2107
|
-
rag_similarity_threshold=rag_similarity_threshold,
|
|
2108
|
-
device=device,
|
|
2109
|
-
conversation_id=conversation_id,
|
|
2110
|
-
stream=stream,
|
|
2111
|
-
)
|
|
2112
|
-
return {"messages": output["messages"], "output": output}
|
|
2113
|
-
|
|
2114
|
-
else:
|
|
2115
|
-
output = f"Unknown command: {command_name}"
|
|
2116
|
-
|
|
2117
|
-
subcommands = [f"/{command}"]
|
|
2118
|
-
return {
|
|
2119
|
-
"messages": messages,
|
|
2120
|
-
"output": output,
|
|
2121
|
-
"subcommands": subcommands,
|
|
2122
|
-
"current_npc": current_npc,
|
|
2123
|
-
}
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
def execute_set_command(command: str, value: str) -> str:
|
|
2127
|
-
"""
|
|
2128
|
-
Function Description:
|
|
2129
|
-
This function sets a configuration value in the .npcshrc file.
|
|
2130
|
-
Args:
|
|
2131
|
-
command: The command to execute.
|
|
2132
|
-
value: The value to set.
|
|
2133
|
-
Keyword Args:
|
|
2134
|
-
None
|
|
2135
|
-
Returns:
|
|
2136
|
-
A message indicating the success or failure of the operation.
|
|
2137
|
-
"""
|
|
2138
|
-
|
|
2139
|
-
config_path = os.path.expanduser("~/.npcshrc")
|
|
2140
|
-
|
|
2141
|
-
# Map command to environment variable name
|
|
2142
|
-
var_map = {
|
|
2143
|
-
"model": "NPCSH_CHAT_MODEL",
|
|
2144
|
-
"provider": "NPCSH_CHAT_PROVIDER",
|
|
2145
|
-
"db_path": "NPCSH_DB_PATH",
|
|
2146
|
-
}
|
|
2147
|
-
|
|
2148
|
-
if command not in var_map:
|
|
2149
|
-
return f"Unknown setting: {command}"
|
|
2150
|
-
|
|
2151
|
-
env_var = var_map[command]
|
|
2152
|
-
|
|
2153
|
-
# Read the current configuration
|
|
2154
|
-
if os.path.exists(config_path):
|
|
2155
|
-
with open(config_path, "r") as f:
|
|
2156
|
-
lines = f.readlines()
|
|
2157
|
-
else:
|
|
2158
|
-
lines = []
|
|
2159
|
-
|
|
2160
|
-
# Check if the property exists and update it, or add it if it doesn't exist
|
|
2161
|
-
property_exists = False
|
|
2162
|
-
for i, line in enumerate(lines):
|
|
2163
|
-
if line.startswith(f"export {env_var}="):
|
|
2164
|
-
lines[i] = f"export {env_var}='{value}'\n"
|
|
2165
|
-
property_exists = True
|
|
2166
|
-
break
|
|
2167
|
-
|
|
2168
|
-
if not property_exists:
|
|
2169
|
-
lines.append(f"export {env_var}='{value}'\n")
|
|
2170
|
-
|
|
2171
|
-
# Save the updated configuration
|
|
2172
|
-
with open(config_path, "w") as f:
|
|
2173
|
-
f.writelines(lines)
|
|
2174
|
-
|
|
2175
|
-
return f"{command.capitalize()} has been set to: {value}"
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
def flush_messages(n: int, messages: list) -> dict:
|
|
2179
|
-
if n <= 0:
|
|
2180
|
-
return {
|
|
2181
|
-
"messages": messages,
|
|
2182
|
-
"output": "Error: 'n' must be a positive integer.",
|
|
2183
|
-
}
|
|
2184
|
-
|
|
2185
|
-
removed_count = min(n, len(messages)) # Calculate how many to remove
|
|
2186
|
-
del messages[-removed_count:] # Remove the last n messages
|
|
2187
|
-
|
|
2188
|
-
return {
|
|
2189
|
-
"messages": messages,
|
|
2190
|
-
"output": f"Flushed {removed_count} message(s). Context count is now {len(messages)} messages.",
|
|
2191
|
-
}
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
def rehash_last_message(
|
|
2195
|
-
conversation_id: str,
|
|
2196
|
-
model: str,
|
|
2197
|
-
provider: str,
|
|
2198
|
-
npc: Any = None,
|
|
2199
|
-
stream: bool = False,
|
|
2200
|
-
) -> dict:
|
|
2201
|
-
# Fetch the last message or command related to this conversation ID
|
|
2202
|
-
command_history = CommandHistory()
|
|
2203
|
-
last_message = command_history.get_last_conversation(conversation_id)
|
|
2204
|
-
if last_message is None:
|
|
2205
|
-
convo_id = command_history.get_most_recent_conversation_id()[0]
|
|
2206
|
-
last_message = command_history.get_last_conversation(convo_id)
|
|
2207
|
-
|
|
2208
|
-
user_command = last_message[3] # Assuming content is in the 4th column
|
|
2209
|
-
return check_llm_command(
|
|
2210
|
-
user_command,
|
|
2211
|
-
model=model,
|
|
2212
|
-
provider=provider,
|
|
2213
|
-
npc=npc,
|
|
2214
|
-
messages=None,
|
|
2215
|
-
stream=stream,
|
|
2216
|
-
)
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
def get_npc_from_command(command: str) -> Optional[str]:
|
|
2220
|
-
"""
|
|
2221
|
-
Function Description:
|
|
2222
|
-
This function extracts the NPC name from a command string.
|
|
2223
|
-
Args:
|
|
2224
|
-
command: The command string.
|
|
2225
|
-
|
|
2226
|
-
Keyword Args:
|
|
2227
|
-
None
|
|
2228
|
-
Returns:
|
|
2229
|
-
The NPC name if found, or None
|
|
2230
|
-
"""
|
|
2231
|
-
|
|
2232
|
-
parts = command.split()
|
|
2233
|
-
npc = None
|
|
2234
|
-
for part in parts:
|
|
2235
|
-
if part.startswith("npc="):
|
|
2236
|
-
npc = part.split("=")[1]
|
|
2237
|
-
break
|
|
2238
|
-
return npc
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
def open_terminal_editor(command: str) -> None:
|
|
2242
|
-
"""
|
|
2243
|
-
Function Description:
|
|
2244
|
-
This function opens a terminal-based text editor.
|
|
2245
|
-
Args:
|
|
2246
|
-
command: The command to open the editor.
|
|
2247
|
-
Keyword Args:
|
|
2248
|
-
None
|
|
2249
|
-
Returns:
|
|
2250
|
-
None
|
|
2251
|
-
"""
|
|
2252
|
-
|
|
2253
|
-
try:
|
|
2254
|
-
os.system(command)
|
|
2255
|
-
except Exception as e:
|
|
2256
|
-
print(f"Error opening terminal editor: {e}")
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
def parse_piped_command(current_command):
|
|
2260
|
-
"""
|
|
2261
|
-
Parse a single command for additional arguments.
|
|
2262
|
-
"""
|
|
2263
|
-
# Use shlex to handle complex argument parsing
|
|
2264
|
-
if "/" not in current_command:
|
|
2265
|
-
return current_command, []
|
|
2266
|
-
|
|
2267
|
-
try:
|
|
2268
|
-
command_parts = shlex.split(current_command)
|
|
2269
|
-
# print(command_parts)
|
|
2270
|
-
except ValueError:
|
|
2271
|
-
# Fallback if quote parsing fails
|
|
2272
|
-
command_parts = current_command.split()
|
|
2273
|
-
# print(command_parts)
|
|
2274
|
-
# Base command is the first part
|
|
2275
|
-
base_command = command_parts[0]
|
|
2276
|
-
|
|
2277
|
-
# Additional arguments are the rest
|
|
2278
|
-
additional_args = command_parts[1:] if len(command_parts) > 1 else []
|
|
2279
|
-
|
|
2280
|
-
return base_command, additional_args
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
def replace_pipe_outputs(command: str, piped_outputs: list, cmd_idx: int) -> str:
|
|
2284
|
-
"""
|
|
2285
|
-
Replace {0}, {1}, etc. placeholders with actual piped outputs.
|
|
2286
|
-
|
|
2287
|
-
Args:
|
|
2288
|
-
command (str): Command with potential {n} placeholders
|
|
2289
|
-
piped_outputs (list): List of outputs from previous commands
|
|
2290
|
-
|
|
2291
|
-
Returns:
|
|
2292
|
-
str: Command with placeholders replaced
|
|
2293
|
-
"""
|
|
2294
|
-
placeholders = [f"{{{cmd_idx-1}}}", f"'{{{cmd_idx-1}}}'", f'"{{{cmd_idx-1}}}"']
|
|
2295
|
-
if str(cmd_idx - 1) in command:
|
|
2296
|
-
for placeholder in placeholders:
|
|
2297
|
-
command = command.replace(placeholder, str(output))
|
|
2298
|
-
elif cmd_idx > 0 and len(piped_outputs) > 0:
|
|
2299
|
-
# assume to pipe the previous commands output to the next command
|
|
2300
|
-
command = command + " " + str(piped_outputs[-1])
|
|
2301
|
-
return command
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
def execute_command(
|
|
2305
|
-
command: str,
|
|
2306
|
-
db_path: str,
|
|
2307
|
-
npc_compiler: NPCCompiler,
|
|
2308
|
-
current_npc: NPC = None,
|
|
2309
|
-
model: str = None,
|
|
2310
|
-
provider: str = None,
|
|
2311
|
-
api_key: str = None,
|
|
2312
|
-
api_url: str = None,
|
|
2313
|
-
messages: list = None,
|
|
2314
|
-
conversation_id: str = None,
|
|
2315
|
-
stream: bool = False,
|
|
2316
|
-
embedding_model=None,
|
|
2317
|
-
):
|
|
2318
|
-
"""
|
|
2319
|
-
Function Description:
|
|
2320
|
-
Executes a command, with support for piping outputs between commands.
|
|
2321
|
-
Args:
|
|
2322
|
-
command : str : Command
|
|
2323
|
-
|
|
2324
|
-
db_path : str : Database path
|
|
2325
|
-
npc_compiler : NPCCompiler : NPC compiler
|
|
2326
|
-
Keyword Args:
|
|
2327
|
-
embedding_model : Embedding model
|
|
2328
|
-
current_npc : NPC : Current NPC
|
|
2329
|
-
messages : list : Messages
|
|
2330
|
-
Returns:
|
|
2331
|
-
dict : dict : Dictionary
|
|
2332
|
-
"""
|
|
2333
|
-
subcommands = []
|
|
2334
|
-
output = ""
|
|
2335
|
-
location = os.getcwd()
|
|
2336
|
-
db_conn = sqlite3.connect(db_path)
|
|
2337
|
-
# print(f"Executing command: {command}")
|
|
2338
|
-
if len(command.strip()) == 0:
|
|
2339
|
-
return {"messages": messages, "output": output, "current_npc": current_npc}
|
|
2340
|
-
|
|
2341
|
-
if messages is None:
|
|
2342
|
-
messages = []
|
|
2343
|
-
|
|
2344
|
-
# Split commands by pipe, preserving the original parsing logic
|
|
2345
|
-
commands = command.split("|")
|
|
2346
|
-
# print(commands)
|
|
2347
|
-
available_models = get_available_models()
|
|
2348
|
-
|
|
2349
|
-
# Track piped output between commands
|
|
2350
|
-
piped_outputs = []
|
|
2351
|
-
|
|
2352
|
-
for idx, single_command in enumerate(commands):
|
|
2353
|
-
# Modify command if there's piped output from previous command
|
|
2354
|
-
|
|
2355
|
-
if idx > 0:
|
|
2356
|
-
single_command, additional_args = parse_piped_command(single_command)
|
|
2357
|
-
if len(piped_outputs) > 0:
|
|
2358
|
-
single_command = replace_pipe_outputs(
|
|
2359
|
-
single_command, piped_outputs, idx
|
|
2360
|
-
)
|
|
2361
|
-
if len(additional_args) > 0:
|
|
2362
|
-
single_command = f"{single_command} {' '.join(additional_args)}"
|
|
2363
|
-
|
|
2364
|
-
messages.append({"role": "user", "content": single_command})
|
|
2365
|
-
# print(messages)
|
|
2366
|
-
|
|
2367
|
-
if model is None:
|
|
2368
|
-
# note the only situation where id expect this to take precedent is when a frontend is specifying the model
|
|
2369
|
-
# to pass through at each time
|
|
2370
|
-
model_override, provider_override, command = get_model_and_provider(
|
|
2371
|
-
single_command, available_models[0]
|
|
2372
|
-
)
|
|
2373
|
-
if model_override is None:
|
|
2374
|
-
model_override = os.getenv("NPCSH_CHAT_MODEL")
|
|
2375
|
-
if provider_override is None:
|
|
2376
|
-
provider_override = os.getenv("NPCSH_CHAT_PROVIDER")
|
|
2377
|
-
else:
|
|
2378
|
-
model_override = model
|
|
2379
|
-
provider_override = provider
|
|
2380
|
-
|
|
2381
|
-
# Rest of the existing logic remains EXACTLY the same
|
|
2382
|
-
# print(model_override, provider_override)
|
|
2383
|
-
|
|
2384
|
-
if current_npc is None:
|
|
2385
|
-
valid_npcs = get_db_npcs(db_path)
|
|
2386
|
-
|
|
2387
|
-
npc_name = get_npc_from_command(command)
|
|
2388
|
-
|
|
2389
|
-
if npc_name is None:
|
|
2390
|
-
npc_name = "sibiji" # Default NPC
|
|
2391
|
-
npc_path = get_npc_path(npc_name, db_path)
|
|
2392
|
-
|
|
2393
|
-
npc = load_npc_from_file(npc_path, db_conn)
|
|
2394
|
-
current_npc = npc
|
|
2395
|
-
else:
|
|
2396
|
-
valid_npcs = [current_npc]
|
|
2397
|
-
npc = current_npc
|
|
2398
|
-
|
|
2399
|
-
# print(single_command.startswith("/"))
|
|
2400
|
-
if single_command.startswith("/"):
|
|
2401
|
-
result = execute_slash_command(
|
|
2402
|
-
single_command,
|
|
2403
|
-
db_path,
|
|
2404
|
-
db_conn,
|
|
2405
|
-
npc_compiler,
|
|
2406
|
-
valid_npcs,
|
|
2407
|
-
npc=npc,
|
|
2408
|
-
messages=messages,
|
|
2409
|
-
model=model_override,
|
|
2410
|
-
provider=provider_override,
|
|
2411
|
-
conversation_id=conversation_id,
|
|
2412
|
-
stream=stream,
|
|
2413
|
-
)
|
|
2414
|
-
## deal with stream here
|
|
2415
|
-
|
|
2416
|
-
output = result.get("output", "")
|
|
2417
|
-
new_messages = result.get("messages", None)
|
|
2418
|
-
subcommands = result.get("subcommands", [])
|
|
2419
|
-
current_npc = result.get("current_npc", None)
|
|
2420
|
-
|
|
2421
|
-
else:
|
|
2422
|
-
# print(single_command)
|
|
2423
|
-
try:
|
|
2424
|
-
command_parts = shlex.split(single_command)
|
|
2425
|
-
# print(command_parts)
|
|
2426
|
-
except ValueError as e:
|
|
2427
|
-
if "No closing quotation" in str(e):
|
|
2428
|
-
# Attempt to close unclosed quotes
|
|
2429
|
-
if single_command.count('"') % 2 == 1:
|
|
2430
|
-
single_command += '"'
|
|
2431
|
-
elif single_command.count("'") % 2 == 1:
|
|
2432
|
-
single_command += "'"
|
|
2433
|
-
try:
|
|
2434
|
-
command_parts = shlex.split(single_command)
|
|
2435
|
-
except ValueError:
|
|
2436
|
-
# fall back to regular split
|
|
2437
|
-
command_parts = single_command.split()
|
|
2438
|
-
|
|
2439
|
-
# ALL EXISTING COMMAND HANDLING LOGIC REMAINS UNCHANGED
|
|
2440
|
-
if command_parts[0] in interactive_commands:
|
|
2441
|
-
print(f"Starting interactive {command_parts[0]} session...")
|
|
2442
|
-
return_code = start_interactive_session(
|
|
2443
|
-
interactive_commands[command_parts[0]]
|
|
2444
|
-
)
|
|
2445
|
-
return {
|
|
2446
|
-
"messages": messages,
|
|
2447
|
-
"output": f"Interactive {command_parts[0]} session ended with return code {return_code}",
|
|
2448
|
-
"current_npc": current_npc,
|
|
2449
|
-
}
|
|
2450
|
-
elif command_parts[0] == "cd":
|
|
2451
|
-
change_dir_result = change_directory(command_parts, messages)
|
|
2452
|
-
messages = change_dir_result["messages"]
|
|
2453
|
-
output = change_dir_result["output"]
|
|
2454
|
-
elif command_parts[0] in BASH_COMMANDS:
|
|
2455
|
-
if command_parts[0] in TERMINAL_EDITORS:
|
|
2456
|
-
return {
|
|
2457
|
-
"messages": messages,
|
|
2458
|
-
"output": open_terminal_editor(command),
|
|
2459
|
-
"current_npc": current_npc,
|
|
2460
|
-
}
|
|
2461
|
-
elif command_parts[0] in ["cat", "find", "who", "open", "which"]:
|
|
2462
|
-
if not validate_bash_command(command_parts):
|
|
2463
|
-
output = "Error: Invalid command syntax or arguments"
|
|
2464
|
-
output = check_llm_command(
|
|
2465
|
-
command,
|
|
2466
|
-
npc=npc,
|
|
2467
|
-
messages=messages,
|
|
2468
|
-
model=model_override,
|
|
2469
|
-
provider=provider_override,
|
|
2470
|
-
stream=stream,
|
|
2471
|
-
)
|
|
2472
|
-
## deal with stream here
|
|
2473
|
-
|
|
2474
|
-
else:
|
|
2475
|
-
# ALL THE EXISTING SUBPROCESS AND SPECIFIC COMMAND CHECKS REMAIN
|
|
2476
|
-
try:
|
|
2477
|
-
result = subprocess.run(
|
|
2478
|
-
command_parts, capture_output=True, text=True
|
|
2479
|
-
)
|
|
2480
|
-
output = result.stdout + result.stderr
|
|
2481
|
-
except Exception as e:
|
|
2482
|
-
output = f"Error executing command: {e}"
|
|
2483
|
-
|
|
2484
|
-
# The entire existing 'open' handling remains exactly the same
|
|
2485
|
-
elif command.startswith("open "):
|
|
2486
|
-
try:
|
|
2487
|
-
path_to_open = os.path.expanduser(
|
|
2488
|
-
single_command.split(" ", 1)[1]
|
|
2489
|
-
)
|
|
2490
|
-
absolute_path = os.path.abspath(path_to_open)
|
|
2491
|
-
expanded_command = [
|
|
2492
|
-
"open",
|
|
2493
|
-
absolute_path,
|
|
2494
|
-
]
|
|
2495
|
-
subprocess.run(expanded_command, check=True)
|
|
2496
|
-
output = f"Launched: {command}"
|
|
2497
|
-
except subprocess.CalledProcessError as e:
|
|
2498
|
-
output = colored(f"Error opening: {e}", "red")
|
|
2499
|
-
except Exception as e:
|
|
2500
|
-
output = colored(f"Error executing command: {str(e)}", "red")
|
|
2501
|
-
|
|
2502
|
-
# Rest of BASH_COMMANDS handling remains the same
|
|
2503
|
-
else:
|
|
2504
|
-
try:
|
|
2505
|
-
result = subprocess.run(
|
|
2506
|
-
command_parts, capture_output=True, text=True
|
|
2507
|
-
)
|
|
2508
|
-
output = result.stdout
|
|
2509
|
-
if result.stderr:
|
|
2510
|
-
output += colored(f"\nError: {result.stderr}", "red")
|
|
2511
|
-
|
|
2512
|
-
colored_output = ""
|
|
2513
|
-
for line in output.split("\n"):
|
|
2514
|
-
parts = line.split()
|
|
2515
|
-
if parts:
|
|
2516
|
-
filepath = parts[-1]
|
|
2517
|
-
color, attrs = get_file_color(filepath)
|
|
2518
|
-
colored_filepath = colored(filepath, color, attrs=attrs)
|
|
2519
|
-
colored_line = " ".join(parts[:-1] + [colored_filepath])
|
|
2520
|
-
colored_output += colored_line + "\n"
|
|
2521
|
-
else:
|
|
2522
|
-
colored_output += line
|
|
2523
|
-
output = colored_output.rstrip()
|
|
2524
|
-
|
|
2525
|
-
if not output and result.returncode == 0:
|
|
2526
|
-
output = colored(
|
|
2527
|
-
f"Command '{single_command}' executed successfully (no output).",
|
|
2528
|
-
"green",
|
|
2529
|
-
)
|
|
2530
|
-
print(output)
|
|
2531
|
-
except Exception as e:
|
|
2532
|
-
output = colored(f"Error executing command: {e}", "red")
|
|
2533
|
-
|
|
2534
|
-
else:
|
|
2535
|
-
# print("LLM command")
|
|
2536
|
-
# print(single_command)
|
|
2537
|
-
# LLM command processing with existing logic
|
|
2538
|
-
# print(api_key, api_url)
|
|
2539
|
-
output = check_llm_command(
|
|
2540
|
-
single_command,
|
|
2541
|
-
npc=npc,
|
|
2542
|
-
messages=messages,
|
|
2543
|
-
model=model_override,
|
|
2544
|
-
provider=provider_override,
|
|
2545
|
-
stream=stream,
|
|
2546
|
-
api_key=api_key,
|
|
2547
|
-
api_url=api_url,
|
|
2548
|
-
)
|
|
2549
|
-
## deal with stream here
|
|
2550
|
-
|
|
2551
|
-
# Capture output for next piped command
|
|
2552
|
-
if isinstance(output, dict):
|
|
2553
|
-
response = output.get("output", "")
|
|
2554
|
-
new_messages = output.get("messages", None)
|
|
2555
|
-
if new_messages is not None:
|
|
2556
|
-
messages = new_messages
|
|
2557
|
-
output = response
|
|
2558
|
-
|
|
2559
|
-
# Only render markdown once, at the end
|
|
2560
|
-
if output:
|
|
2561
|
-
## deal with stream output here.
|
|
2562
|
-
|
|
2563
|
-
if not stream:
|
|
2564
|
-
try:
|
|
2565
|
-
render_markdown(output)
|
|
2566
|
-
except AttributeError:
|
|
2567
|
-
print(output)
|
|
2568
|
-
|
|
2569
|
-
piped_outputs.append(f'"{output}"')
|
|
2570
|
-
|
|
2571
|
-
try:
|
|
2572
|
-
# Prepare text to embed (both command and response)
|
|
2573
|
-
texts_to_embed = [command, str(output) if output else ""]
|
|
2574
|
-
|
|
2575
|
-
# Generate embeddings
|
|
2576
|
-
embeddings = get_embeddings(
|
|
2577
|
-
texts_to_embed,
|
|
2578
|
-
)
|
|
2579
|
-
|
|
2580
|
-
# Prepare metadata
|
|
2581
|
-
metadata = [
|
|
2582
|
-
{
|
|
2583
|
-
"type": "command",
|
|
2584
|
-
"timestamp": datetime.datetime.now().isoformat(),
|
|
2585
|
-
"path": os.getcwd(),
|
|
2586
|
-
"npc": npc.name if npc else None,
|
|
2587
|
-
"conversation_id": conversation_id,
|
|
2588
|
-
},
|
|
2589
|
-
{
|
|
2590
|
-
"type": "response",
|
|
2591
|
-
"timestamp": datetime.datetime.now().isoformat(),
|
|
2592
|
-
"path": os.getcwd(),
|
|
2593
|
-
"npc": npc.name if npc else None,
|
|
2594
|
-
"conversation_id": conversation_id,
|
|
2595
|
-
},
|
|
2596
|
-
]
|
|
2597
|
-
embedding_model = os.environ.get("NPCSH_EMBEDDING_MODEL")
|
|
2598
|
-
embedding_provider = os.environ.get("NPCSH_EMBEDDING_PROVIDER")
|
|
2599
|
-
collection_name = (
|
|
2600
|
-
f"{embedding_provider}_{embedding_model}_embeddings"
|
|
2601
|
-
)
|
|
2602
|
-
|
|
2603
|
-
try:
|
|
2604
|
-
collection = chroma_client.get_collection(collection_name)
|
|
2605
|
-
except Exception as e:
|
|
2606
|
-
print(f"Warning: Failed to get collection: {str(e)}")
|
|
2607
|
-
print("Creating new collection...")
|
|
2608
|
-
collection = chroma_client.create_collection(collection_name)
|
|
2609
|
-
date_str = datetime.datetime.now().isoformat()
|
|
2610
|
-
# print(date_str)
|
|
2611
|
-
|
|
2612
|
-
# Add to collection
|
|
2613
|
-
current_ids = [f"cmd_{date_str}", f"resp_{date_str}"]
|
|
2614
|
-
collection.add(
|
|
2615
|
-
embeddings=embeddings,
|
|
2616
|
-
documents=texts_to_embed, # Adjust as needed
|
|
2617
|
-
metadatas=metadata, # Adjust as needed
|
|
2618
|
-
ids=current_ids,
|
|
2619
|
-
)
|
|
2620
|
-
|
|
2621
|
-
# print("Stored embeddings.")
|
|
2622
|
-
# print("collection", collection)
|
|
2623
|
-
except Exception as e:
|
|
2624
|
-
print(f"Warning: Failed to store embeddings: {str(e)}")
|
|
2625
|
-
|
|
2626
|
-
# return following
|
|
2627
|
-
# print(current_npc)
|
|
2628
|
-
return {
|
|
2629
|
-
"messages": messages,
|
|
2630
|
-
"output": output,
|
|
2631
|
-
"conversation_id": conversation_id,
|
|
2632
|
-
"model": model,
|
|
2633
|
-
"current_path": os.getcwd(),
|
|
2634
|
-
"provider": provider,
|
|
2635
|
-
"current_npc": current_npc if current_npc else npc,
|
|
2636
|
-
}
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
def execute_command_stream(
|
|
2640
|
-
command: str,
|
|
2641
|
-
db_path: str,
|
|
2642
|
-
npc_compiler: NPCCompiler,
|
|
2643
|
-
embedding_model=None,
|
|
2644
|
-
current_npc: NPC = None,
|
|
2645
|
-
model: str = None,
|
|
2646
|
-
provider: str = None,
|
|
2647
|
-
messages: list = None,
|
|
2648
|
-
conversation_id: str = None,
|
|
2649
|
-
):
|
|
2650
|
-
"""
|
|
2651
|
-
Function Description:
|
|
2652
|
-
Executes a command, with support for piping outputs between commands.
|
|
2653
|
-
Args:
|
|
2654
|
-
command : str : Command
|
|
2655
|
-
|
|
2656
|
-
db_path : str : Database path
|
|
2657
|
-
npc_compiler : NPCCompiler : NPC compiler
|
|
2658
|
-
Keyword Args:
|
|
2659
|
-
embedding_model : Union[SentenceTransformer, Any] : Embedding model
|
|
2660
|
-
current_npc : NPC : Current NPC
|
|
2661
|
-
messages : list : Messages
|
|
2662
|
-
Returns:stream
|
|
2663
|
-
dict : dict : Dictionary
|
|
2664
|
-
"""
|
|
2665
|
-
subcommands = []
|
|
2666
|
-
output = ""
|
|
2667
|
-
location = os.getcwd()
|
|
2668
|
-
db_conn = sqlite3.connect(db_path)
|
|
2669
|
-
|
|
2670
|
-
# Split commands by pipe, preserving the original parsing logic
|
|
2671
|
-
commands = command.split("|")
|
|
2672
|
-
available_models = get_available_models()
|
|
2673
|
-
|
|
2674
|
-
# Track piped output between commands
|
|
2675
|
-
piped_outputs = []
|
|
2676
|
-
|
|
2677
|
-
for idx, single_command in enumerate(commands):
|
|
2678
|
-
# Modify command if there's piped output from previous command
|
|
2679
|
-
if idx > 0:
|
|
2680
|
-
single_command, additional_args = parse_piped_command(single_command)
|
|
2681
|
-
if len(piped_outputs) > 0:
|
|
2682
|
-
single_command = replace_pipe_outputs(
|
|
2683
|
-
single_command, piped_outputs, idx
|
|
2684
|
-
)
|
|
2685
|
-
if len(additional_args) > 0:
|
|
2686
|
-
single_command = f"{single_command} {' '.join(additional_args)}"
|
|
2687
|
-
messages.append({"role": "user", "content": single_command})
|
|
2688
|
-
if model is None:
|
|
2689
|
-
# note the only situation where id expect this to take precedent is when a frontend is specifying the model
|
|
2690
|
-
# to pass through at each time
|
|
2691
|
-
model_override, provider_override, command = get_model_and_provider(
|
|
2692
|
-
single_command, available_models[0]
|
|
2693
|
-
)
|
|
2694
|
-
if model_override is None:
|
|
2695
|
-
model_override = os.getenv("NPCSH_CHAT_MODEL")
|
|
2696
|
-
if provider_override is None:
|
|
2697
|
-
provider_override = os.getenv("NPCSH_CHAT_PROVIDER")
|
|
2698
|
-
else:
|
|
2699
|
-
model_override = model
|
|
2700
|
-
provider_override = provider
|
|
2701
|
-
|
|
2702
|
-
# Rest of the existing logic remains EXACTLY the same
|
|
2703
|
-
# print(model_override, provider_override)
|
|
2704
|
-
if current_npc is None:
|
|
2705
|
-
valid_npcs = get_db_npcs(db_path)
|
|
2706
|
-
|
|
2707
|
-
npc_name = get_npc_from_command(command)
|
|
2708
|
-
if npc_name is None:
|
|
2709
|
-
npc_name = "sibiji" # Default NPC
|
|
2710
|
-
npc_path = get_npc_path(npc_name, db_path)
|
|
2711
|
-
npc = load_npc_from_file(npc_path, db_conn)
|
|
2712
|
-
else:
|
|
2713
|
-
npc = current_npc
|
|
2714
|
-
# print(single_command.startswith("/"))
|
|
2715
|
-
if single_command.startswith("/"):
|
|
2716
|
-
return execute_slash_command(
|
|
2717
|
-
single_command,
|
|
2718
|
-
db_path,
|
|
2719
|
-
db_conn,
|
|
2720
|
-
npc_compiler,
|
|
2721
|
-
valid_npcs,
|
|
2722
|
-
npc=npc,
|
|
2723
|
-
messages=messages,
|
|
2724
|
-
model=model_override,
|
|
2725
|
-
provider=provider_override,
|
|
2726
|
-
conversation_id=conversation_id,
|
|
2727
|
-
stream=True,
|
|
2728
|
-
)
|
|
2729
|
-
else: # LLM command processing with existing logic
|
|
2730
|
-
return check_llm_command(
|
|
2731
|
-
single_command,
|
|
2732
|
-
npc=npc,
|
|
2733
|
-
messages=messages,
|
|
2734
|
-
model=model_override,
|
|
2735
|
-
provider=provider_override,
|
|
2736
|
-
stream=True,
|
|
2737
|
-
)
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
def enter_whisper_mode(
|
|
2741
|
-
messages: list = None,
|
|
2742
|
-
npc: Any = None,
|
|
2743
|
-
spool=False,
|
|
2744
|
-
continuous=False,
|
|
2745
|
-
stream=False,
|
|
2746
|
-
) -> str:
|
|
2747
|
-
"""
|
|
2748
|
-
Function Description:
|
|
2749
|
-
This function is used to enter the whisper mode.
|
|
2750
|
-
Args:
|
|
2751
|
-
Keyword Args:
|
|
2752
|
-
npc : Any : The NPC object.
|
|
2753
|
-
Returns:
|
|
2754
|
-
str : The output of the whisper mode.
|
|
2755
|
-
"""
|
|
2756
|
-
|
|
2757
|
-
try:
|
|
2758
|
-
model = whisper.load_model("base")
|
|
2759
|
-
except Exception as e:
|
|
2760
|
-
return f"Error: Unable to load Whisper model due to {str(e)}"
|
|
2761
|
-
|
|
2762
|
-
whisper_output = []
|
|
2763
|
-
if npc:
|
|
2764
|
-
npc_info = f" (NPC: {npc.name})"
|
|
2765
|
-
else:
|
|
2766
|
-
npc_info = ""
|
|
2767
|
-
|
|
2768
|
-
if messages is None:
|
|
2769
|
-
messages = [] # Initialize messages list if not provided
|
|
2770
|
-
|
|
2771
|
-
# Begin whisper mode functionality
|
|
2772
|
-
whisper_output.append(
|
|
2773
|
-
f"Entering whisper mode{npc_info}. Calibrating silence level..."
|
|
2774
|
-
)
|
|
2775
|
-
|
|
2776
|
-
try:
|
|
2777
|
-
silence_threshold = calibrate_silence()
|
|
2778
|
-
except Exception as e:
|
|
2779
|
-
return f"Error: Unable to calibrate silence due to {str(e)}"
|
|
2780
|
-
|
|
2781
|
-
whisper_output.append(
|
|
2782
|
-
"Ready. Speak after seeing 'Listening...'. Say 'exit' or type '/wq' to quit."
|
|
2783
|
-
)
|
|
2784
|
-
speak_text("Whisper mode activated. Ready for your input.")
|
|
2785
|
-
|
|
2786
|
-
while True:
|
|
2787
|
-
audio_data = record_audio(silence_threshold=silence_threshold)
|
|
2788
|
-
with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as temp_audio:
|
|
2789
|
-
wf = wave.open(temp_audio.name, "wb")
|
|
2790
|
-
wf.setnchannels(1)
|
|
2791
|
-
wf.setsampwidth(2)
|
|
2792
|
-
wf.setframerate(16000)
|
|
2793
|
-
wf.writeframes(audio_data)
|
|
2794
|
-
wf.close()
|
|
2795
|
-
|
|
2796
|
-
result = model.transcribe(temp_audio.name)
|
|
2797
|
-
text = result["text"].strip()
|
|
2798
|
-
print(f"You said: {text}")
|
|
2799
|
-
os.unlink(temp_audio.name)
|
|
2800
|
-
|
|
2801
|
-
messages.append({"role": "user", "content": text}) # Add user message
|
|
2802
|
-
if text.lower() in ["exit", "/wq"]:
|
|
2803
|
-
whisper_output.append("Exiting whisper mode.")
|
|
2804
|
-
speak_text("Exiting whisper mode. Goodbye!")
|
|
2805
|
-
break
|
|
2806
|
-
if not spool:
|
|
2807
|
-
llm_response = check_llm_command(
|
|
2808
|
-
text, npc=npc, messages=messages, stream=stream
|
|
2809
|
-
) # Use
|
|
2810
|
-
|
|
2811
|
-
messages = llm_response["messages"]
|
|
2812
|
-
output = llm_response["output"]
|
|
2813
|
-
else:
|
|
2814
|
-
if stream:
|
|
2815
|
-
messages = get_stream(
|
|
2816
|
-
messages,
|
|
2817
|
-
model=model,
|
|
2818
|
-
provider=provider,
|
|
2819
|
-
npc=npc,
|
|
2820
|
-
)
|
|
2821
|
-
else:
|
|
2822
|
-
messages = get_conversation(
|
|
2823
|
-
messages,
|
|
2824
|
-
model=model,
|
|
2825
|
-
provider=provider,
|
|
2826
|
-
npc=npc,
|
|
2827
|
-
)
|
|
2828
|
-
|
|
2829
|
-
output = messages[-1]["content"]
|
|
2830
|
-
print(output)
|
|
2831
|
-
if not continuous:
|
|
2832
|
-
inp = input("Press Enter to continue or type '/q' to quit: ")
|
|
2833
|
-
if inp.lower() == "/q":
|
|
2834
|
-
break
|
|
2835
|
-
|
|
2836
|
-
return messages
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
def enter_notes_mode(npc: Any = None) -> None:
|
|
2840
|
-
"""
|
|
2841
|
-
Function Description:
|
|
2842
|
-
|
|
2843
|
-
Args:
|
|
2844
|
-
|
|
2845
|
-
Keyword Args:
|
|
2846
|
-
npc : Any : The NPC object.
|
|
2847
|
-
Returns:
|
|
2848
|
-
None
|
|
2849
|
-
|
|
2850
|
-
"""
|
|
2851
|
-
|
|
2852
|
-
npc_name = npc.name if npc else "sibiji"
|
|
2853
|
-
print(f"Entering notes mode (NPC: {npc_name}). Type '/nq' to exit.")
|
|
2854
|
-
|
|
2855
|
-
while True:
|
|
2856
|
-
note = input("Enter your note (or '/nq' to quit): ").strip()
|
|
2857
|
-
|
|
2858
|
-
if note.lower() == "/nq":
|
|
2859
|
-
break
|
|
2860
|
-
|
|
2861
|
-
save_note(note, npc)
|
|
2862
|
-
|
|
2863
|
-
print("Exiting notes mode.")
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
def save_note(note: str, db_conn, npc: Any = None) -> None:
|
|
2867
|
-
"""
|
|
2868
|
-
Function Description:
|
|
2869
|
-
This function is used to save a note.
|
|
2870
|
-
Args:
|
|
2871
|
-
note : str : The note to save.
|
|
2872
|
-
|
|
2873
|
-
Keyword Args:
|
|
2874
|
-
npc : Any : The NPC object.
|
|
2875
|
-
Returns:
|
|
2876
|
-
None
|
|
2877
|
-
"""
|
|
2878
|
-
current_dir = os.getcwd()
|
|
2879
|
-
timestamp = datetime.datetime.now().isoformat()
|
|
2880
|
-
npc_name = npc.name if npc else "base"
|
|
2881
|
-
cursor = conn.cursor()
|
|
2882
|
-
|
|
2883
|
-
# Create notes table if it doesn't exist
|
|
2884
|
-
cursor.execute(
|
|
2885
|
-
"""
|
|
2886
|
-
CREATE TABLE IF NOT EXISTS notes (
|
|
2887
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2888
|
-
timestamp TEXT,
|
|
2889
|
-
note TEXT,
|
|
2890
|
-
npc TEXT,
|
|
2891
|
-
directory TEXT
|
|
2892
|
-
)
|
|
2893
|
-
"""
|
|
2894
|
-
)
|
|
2895
|
-
|
|
2896
|
-
# Insert the note into the database
|
|
2897
|
-
cursor.execute(
|
|
2898
|
-
"""
|
|
2899
|
-
INSERT INTO notes (timestamp, note, npc, directory)
|
|
2900
|
-
VALUES (?, ?, ?, ?)
|
|
2901
|
-
""",
|
|
2902
|
-
(timestamp, note, npc_name, current_dir),
|
|
2903
|
-
)
|
|
2904
|
-
|
|
2905
|
-
conn.commit()
|
|
2906
|
-
|
|
2907
|
-
print("Note saved to database.")
|
|
2908
|
-
# save the note with the current datestamp to the current working directory
|
|
2909
|
-
with open(f"{current_dir}/note_{timestamp}.txt", "w") as f:
|
|
2910
|
-
f.write(note)
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
def enter_data_analysis_mode(npc: Any = None) -> None:
|
|
2914
|
-
"""
|
|
2915
|
-
Function Description:
|
|
2916
|
-
This function is used to enter the data analysis mode.
|
|
2917
|
-
Args:
|
|
2918
|
-
|
|
2919
|
-
Keyword Args:
|
|
2920
|
-
npc : Any : The NPC object.
|
|
2921
|
-
Returns:
|
|
2922
|
-
None
|
|
2923
|
-
"""
|
|
2924
|
-
|
|
2925
|
-
npc_name = npc.name if npc else "data_analyst"
|
|
2926
|
-
print(f"Entering data analysis mode (NPC: {npc_name}). Type '/daq' to exit.")
|
|
2927
|
-
|
|
2928
|
-
dataframes = {} # Dict to store dataframes by name
|
|
2929
|
-
context = {"dataframes": dataframes} # Context to store variables
|
|
2930
|
-
messages = [] # For conversation history if needed
|
|
2931
|
-
|
|
2932
|
-
while True:
|
|
2933
|
-
user_input = input(f"{npc_name}> ").strip()
|
|
2934
|
-
|
|
2935
|
-
if user_input.lower() == "/daq":
|
|
2936
|
-
break
|
|
2937
|
-
|
|
2938
|
-
# Add user input to messages for context if interacting with LLM
|
|
2939
|
-
messages.append({"role": "user", "content": user_input})
|
|
2940
|
-
|
|
2941
|
-
# Process commands
|
|
2942
|
-
if user_input.lower().startswith("load "):
|
|
2943
|
-
# Command format: load <file_path> as <df_name>
|
|
2944
|
-
try:
|
|
2945
|
-
parts = user_input.split()
|
|
2946
|
-
file_path = parts[1]
|
|
2947
|
-
if "as" in parts:
|
|
2948
|
-
as_index = parts.index("as")
|
|
2949
|
-
df_name = parts[as_index + 1]
|
|
2950
|
-
else:
|
|
2951
|
-
df_name = "df" # Default dataframe name
|
|
2952
|
-
# Load data into dataframe
|
|
2953
|
-
df = pd.read_csv(file_path)
|
|
2954
|
-
dataframes[df_name] = df
|
|
2955
|
-
print(f"Data loaded into dataframe '{df_name}'")
|
|
2956
|
-
except Exception as e:
|
|
2957
|
-
print(f"Error loading data: {e}")
|
|
2958
|
-
|
|
2959
|
-
elif user_input.lower().startswith("sql "):
|
|
2960
|
-
# Command format: sql <SQL query>
|
|
2961
|
-
try:
|
|
2962
|
-
query = user_input[4:] # Remove 'sql ' prefix
|
|
2963
|
-
df = pd.read_sql_query(query, npc.db_conn)
|
|
2964
|
-
print(df)
|
|
2965
|
-
# Optionally store result in a dataframe
|
|
2966
|
-
dataframes["sql_result"] = df
|
|
2967
|
-
print("Result stored in dataframe 'sql_result'")
|
|
2968
|
-
|
|
2969
|
-
except Exception as e:
|
|
2970
|
-
print(f"Error executing SQL query: {e}")
|
|
2971
|
-
|
|
2972
|
-
elif user_input.lower().startswith("plot "):
|
|
2973
|
-
# Command format: plot <pandas plotting code>
|
|
2974
|
-
try:
|
|
2975
|
-
code = user_input[5:] # Remove 'plot ' prefix
|
|
2976
|
-
# Prepare execution environment
|
|
2977
|
-
exec_globals = {"pd": pd, "plt": plt, **dataframes}
|
|
2978
|
-
exec(code, exec_globals)
|
|
2979
|
-
plt.show()
|
|
2980
|
-
except Exception as e:
|
|
2981
|
-
print(f"Error generating plot: {e}")
|
|
2982
|
-
|
|
2983
|
-
elif user_input.lower().startswith("exec "):
|
|
2984
|
-
# Command format: exec <Python code>
|
|
2985
|
-
try:
|
|
2986
|
-
code = user_input[5:] # Remove 'exec ' prefix
|
|
2987
|
-
# Prepare execution environment
|
|
2988
|
-
exec_globals = {"pd": pd, "plt": plt, **dataframes}
|
|
2989
|
-
exec(code, exec_globals)
|
|
2990
|
-
# Update dataframes with any new or modified dataframes
|
|
2991
|
-
dataframes.update(
|
|
2992
|
-
{
|
|
2993
|
-
k: v
|
|
2994
|
-
for k, v in exec_globals.items()
|
|
2995
|
-
if isinstance(v, pd.DataFrame)
|
|
2996
|
-
}
|
|
2997
|
-
)
|
|
2998
|
-
except Exception as e:
|
|
2999
|
-
print(f"Error executing code: {e}")
|
|
3000
|
-
|
|
3001
|
-
elif user_input.lower().startswith("help"):
|
|
3002
|
-
# Provide help information
|
|
3003
|
-
print(
|
|
3004
|
-
"""
|
|
3005
|
-
Available commands:
|
|
3006
|
-
- load <file_path> as <df_name>: Load CSV data into a dataframe.
|
|
3007
|
-
- sql <SQL query>: Execute SQL query.
|
|
3008
|
-
- plot <pandas plotting code>: Generate plots using matplotlib.
|
|
3009
|
-
- exec <Python code>: Execute arbitrary Python code.
|
|
3010
|
-
- help: Show this help message.
|
|
3011
|
-
- /daq: Exit data analysis mode.
|
|
3012
|
-
"""
|
|
3013
|
-
)
|
|
3014
|
-
|
|
3015
|
-
else:
|
|
3016
|
-
# Unrecognized command
|
|
3017
|
-
print("Unrecognized command. Type 'help' for a list of available commands.")
|
|
3018
|
-
|
|
3019
|
-
print("Exiting data analysis mode.")
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
def enter_data_mode(npc: Any = None) -> None:
|
|
3023
|
-
"""
|
|
3024
|
-
Function Description:
|
|
3025
|
-
This function is used to enter the data mode.
|
|
3026
|
-
Args:
|
|
3027
|
-
|
|
3028
|
-
Keyword Args:
|
|
3029
|
-
npc : Any : The NPC object.
|
|
3030
|
-
Returns:
|
|
3031
|
-
None
|
|
3032
|
-
"""
|
|
3033
|
-
npc_name = npc.name if npc else "data_analyst"
|
|
3034
|
-
print(f"Entering data mode (NPC: {npc_name}). Type '/dq' to exit.")
|
|
3035
|
-
|
|
3036
|
-
exec_env = {
|
|
3037
|
-
"pd": pd,
|
|
3038
|
-
"np": np,
|
|
3039
|
-
"plt": plt,
|
|
3040
|
-
"os": os,
|
|
3041
|
-
"npc": npc,
|
|
3042
|
-
}
|
|
3043
|
-
|
|
3044
|
-
while True:
|
|
3045
|
-
try:
|
|
3046
|
-
user_input = input(f"{npc_name}> ").strip()
|
|
3047
|
-
if user_input.lower() == "/dq":
|
|
3048
|
-
break
|
|
3049
|
-
elif user_input == "":
|
|
3050
|
-
continue
|
|
3051
|
-
|
|
3052
|
-
# First check if input exists in exec_env
|
|
3053
|
-
if user_input in exec_env:
|
|
3054
|
-
result = exec_env[user_input]
|
|
3055
|
-
if result is not None:
|
|
3056
|
-
if isinstance(result, pd.DataFrame):
|
|
3057
|
-
print(result.to_string())
|
|
3058
|
-
else:
|
|
3059
|
-
print(result)
|
|
3060
|
-
continue
|
|
3061
|
-
|
|
3062
|
-
# Then check if it's a natural language query
|
|
3063
|
-
if not any(
|
|
3064
|
-
keyword in user_input
|
|
3065
|
-
for keyword in [
|
|
3066
|
-
"=",
|
|
3067
|
-
"+",
|
|
3068
|
-
"-",
|
|
3069
|
-
"*",
|
|
3070
|
-
"/",
|
|
3071
|
-
"(",
|
|
3072
|
-
")",
|
|
3073
|
-
"[",
|
|
3074
|
-
"]",
|
|
3075
|
-
"{",
|
|
3076
|
-
"}",
|
|
3077
|
-
"import",
|
|
3078
|
-
]
|
|
3079
|
-
):
|
|
3080
|
-
if "df" in exec_env and isinstance(exec_env["df"], pd.DataFrame):
|
|
3081
|
-
df_info = {
|
|
3082
|
-
"shape": exec_env["df"].shape,
|
|
3083
|
-
"columns": list(exec_env["df"].columns),
|
|
3084
|
-
"dtypes": exec_env["df"].dtypes.to_dict(),
|
|
3085
|
-
"head": exec_env["df"].head().to_dict(),
|
|
3086
|
-
"summary": exec_env["df"].describe().to_dict(),
|
|
3087
|
-
}
|
|
3088
|
-
|
|
3089
|
-
analysis_prompt = f"""Based on this DataFrame info: {df_info}
|
|
3090
|
-
Generate Python analysis commands to answer: {user_input}
|
|
3091
|
-
Return each command on a new line. Do not use markdown formatting or code blocks."""
|
|
3092
|
-
|
|
3093
|
-
analysis_response = npc.get_llm_response(analysis_prompt).get(
|
|
3094
|
-
"response", ""
|
|
3095
|
-
)
|
|
3096
|
-
analysis_commands = [
|
|
3097
|
-
cmd.strip()
|
|
3098
|
-
for cmd in analysis_response.replace("```python", "")
|
|
3099
|
-
.replace("```", "")
|
|
3100
|
-
.split("\n")
|
|
3101
|
-
if cmd.strip()
|
|
3102
|
-
]
|
|
3103
|
-
results = []
|
|
3104
|
-
|
|
3105
|
-
print("\nAnalyzing data...")
|
|
3106
|
-
for cmd in analysis_commands:
|
|
3107
|
-
if cmd.strip():
|
|
3108
|
-
try:
|
|
3109
|
-
result = eval(cmd, exec_env)
|
|
3110
|
-
if result is not None:
|
|
3111
|
-
render_markdown(f"\n{cmd} ")
|
|
3112
|
-
if isinstance(result, pd.DataFrame):
|
|
3113
|
-
render_markdown(result.to_string())
|
|
3114
|
-
else:
|
|
3115
|
-
render_markdown(result)
|
|
3116
|
-
results.append((cmd, result))
|
|
3117
|
-
except SyntaxError:
|
|
3118
|
-
try:
|
|
3119
|
-
exec(cmd, exec_env)
|
|
3120
|
-
except Exception as e:
|
|
3121
|
-
print(f"Error in {cmd}: {str(e)}")
|
|
3122
|
-
except Exception as e:
|
|
3123
|
-
print(f"Error in {cmd}: {str(e)}")
|
|
3124
|
-
|
|
3125
|
-
if results:
|
|
3126
|
-
interpretation_prompt = f"""Based on these analysis results:
|
|
3127
|
-
{[(cmd, str(result)) for cmd, result in results]}
|
|
3128
|
-
|
|
3129
|
-
Provide a clear, concise interpretation of what we found in the data.
|
|
3130
|
-
Focus on key insights and patterns. Do not use markdown formatting."""
|
|
3131
|
-
|
|
3132
|
-
print("\nInterpretation:")
|
|
3133
|
-
interpretation = npc.get_llm_response(
|
|
3134
|
-
interpretation_prompt
|
|
3135
|
-
).get("response", "")
|
|
3136
|
-
interpretation = interpretation.replace("```", "").strip()
|
|
3137
|
-
render_markdown(interpretation)
|
|
3138
|
-
continue
|
|
3139
|
-
|
|
3140
|
-
# If not in exec_env and not natural language, try as Python code
|
|
3141
|
-
try:
|
|
3142
|
-
result = eval(user_input, exec_env)
|
|
3143
|
-
if result is not None:
|
|
3144
|
-
if isinstance(result, pd.DataFrame):
|
|
3145
|
-
print(result.to_string())
|
|
3146
|
-
else:
|
|
3147
|
-
print(result)
|
|
3148
|
-
except SyntaxError:
|
|
3149
|
-
exec(user_input, exec_env)
|
|
3150
|
-
except Exception as e:
|
|
3151
|
-
print(f"Error: {str(e)}")
|
|
3152
|
-
|
|
3153
|
-
except KeyboardInterrupt:
|
|
3154
|
-
print("\nKeyboardInterrupt detected. Exiting data mode.")
|
|
3155
|
-
break
|
|
3156
|
-
except Exception as e:
|
|
3157
|
-
print(f"Error: {str(e)}")
|
|
3158
|
-
|
|
3159
|
-
return
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
def enter_spool_mode(
|
|
3163
|
-
inherit_last: int = 0,
|
|
3164
|
-
model: str = None,
|
|
3165
|
-
provider: str = None,
|
|
3166
|
-
npc: Any = None,
|
|
3167
|
-
files: List[str] = None, # New files parameter
|
|
3168
|
-
rag_similarity_threshold: float = 0.3,
|
|
3169
|
-
device: str = "cpu",
|
|
3170
|
-
messages: List[Dict] = None,
|
|
3171
|
-
conversation_id: str = None,
|
|
3172
|
-
stream: bool = False,
|
|
3173
|
-
) -> Dict:
|
|
3174
|
-
"""
|
|
3175
|
-
Function Description:
|
|
3176
|
-
This function is used to enter the spool mode where files can be loaded into memory.
|
|
3177
|
-
Args:
|
|
3178
|
-
|
|
3179
|
-
inherit_last : int : The number of last commands to inherit.
|
|
3180
|
-
npc : Any : The NPC object.
|
|
3181
|
-
files : List[str] : List of file paths to load into the context.
|
|
3182
|
-
Returns:
|
|
3183
|
-
Dict : The messages and output.
|
|
3184
|
-
|
|
3185
|
-
"""
|
|
3186
|
-
|
|
3187
|
-
command_history = CommandHistory()
|
|
3188
|
-
npc_info = f" (NPC: {npc.name})" if npc else ""
|
|
3189
|
-
print(f"Entering spool mode{npc_info}. Type '/sq' to exit spool mode.")
|
|
3190
|
-
|
|
3191
|
-
spool_context = (
|
|
3192
|
-
messages.copy() if messages else []
|
|
3193
|
-
) # Initialize context with messages
|
|
3194
|
-
|
|
3195
|
-
loaded_content = {} # New dictionary to hold loaded content
|
|
3196
|
-
|
|
3197
|
-
# Create conversation ID if not provided
|
|
3198
|
-
if not conversation_id:
|
|
3199
|
-
conversation_id = start_new_conversation()
|
|
3200
|
-
|
|
3201
|
-
command_history = CommandHistory()
|
|
3202
|
-
# Load specified files if any
|
|
3203
|
-
if files:
|
|
3204
|
-
for file in files:
|
|
3205
|
-
extension = os.path.splitext(file)[1].lower()
|
|
3206
|
-
try:
|
|
3207
|
-
if extension == ".pdf":
|
|
3208
|
-
content = load_pdf(file)["texts"].iloc[0]
|
|
3209
|
-
elif extension == ".csv":
|
|
3210
|
-
content = load_csv(file)
|
|
3211
|
-
else:
|
|
3212
|
-
print(f"Unsupported file type: {file}")
|
|
3213
|
-
continue
|
|
3214
|
-
loaded_content[file] = content
|
|
3215
|
-
print(f"Loaded content from: {file}")
|
|
3216
|
-
except Exception as e:
|
|
3217
|
-
print(f"Error loading {file}: {str(e)}")
|
|
3218
|
-
|
|
3219
|
-
# Add system message to context
|
|
3220
|
-
system_message = get_system_message(npc) if npc else "You are a helpful assistant."
|
|
3221
|
-
if len(spool_context) > 0:
|
|
3222
|
-
if spool_context[0]["role"] != "system":
|
|
3223
|
-
spool_context.insert(0, {"role": "system", "content": system_message})
|
|
3224
|
-
else:
|
|
3225
|
-
spool_context.append({"role": "system", "content": system_message})
|
|
3226
|
-
# Inherit last n messages if specified
|
|
3227
|
-
if inherit_last > 0:
|
|
3228
|
-
last_commands = command_history.get_all(limit=inherit_last)
|
|
3229
|
-
for cmd in reversed(last_commands):
|
|
3230
|
-
spool_context.append({"role": "user", "content": cmd[2]})
|
|
3231
|
-
spool_context.append({"role": "assistant", "content": cmd[4]})
|
|
3232
|
-
|
|
3233
|
-
if npc is not None:
|
|
3234
|
-
if model is None:
|
|
3235
|
-
model = npc.model
|
|
3236
|
-
if provider is None:
|
|
3237
|
-
provider = npc.provider
|
|
3238
|
-
|
|
3239
|
-
while True:
|
|
3240
|
-
try:
|
|
3241
|
-
user_input = input("spool> ").strip()
|
|
3242
|
-
if len(user_input) == 0:
|
|
3243
|
-
continue
|
|
3244
|
-
if user_input.lower() == "/sq":
|
|
3245
|
-
print("Exiting spool mode.")
|
|
3246
|
-
break
|
|
3247
|
-
if user_input.lower() == "/rehash": # Check for whisper command
|
|
3248
|
-
# send the most recent message
|
|
3249
|
-
print("Rehashing last message...")
|
|
3250
|
-
output = rehash_last_message(
|
|
3251
|
-
conversation_id,
|
|
3252
|
-
model=model,
|
|
3253
|
-
provider=provider,
|
|
3254
|
-
npc=npc,
|
|
3255
|
-
stream=stream,
|
|
3256
|
-
)
|
|
3257
|
-
print(output["output"])
|
|
3258
|
-
messages = output.get("messages", [])
|
|
3259
|
-
output = output.get("output", "")
|
|
3260
|
-
|
|
3261
|
-
if user_input.lower() == "/whisper": # Check for whisper command
|
|
3262
|
-
messages = enter_whisper_mode(spool_context, npc)
|
|
3263
|
-
# print(messages) # Optionally print output from whisper mode
|
|
3264
|
-
continue # Continue with spool mode after exiting whisper mode
|
|
3265
|
-
|
|
3266
|
-
if user_input.startswith("/ots"):
|
|
3267
|
-
command_parts = user_input.split()
|
|
3268
|
-
file_path = None
|
|
3269
|
-
filename = None
|
|
3270
|
-
|
|
3271
|
-
# Handle image loading/capturing
|
|
3272
|
-
if len(command_parts) > 1:
|
|
3273
|
-
filename = command_parts[1]
|
|
3274
|
-
file_path = os.path.join(os.getcwd(), filename)
|
|
3275
|
-
else:
|
|
3276
|
-
output = capture_screenshot(npc=npc)
|
|
3277
|
-
if output and "file_path" in output:
|
|
3278
|
-
file_path = output["file_path"]
|
|
3279
|
-
filename = output["filename"]
|
|
3280
|
-
|
|
3281
|
-
if not file_path or not os.path.exists(file_path):
|
|
3282
|
-
print(f"Error: Image file not found at {file_path}")
|
|
3283
|
-
continue
|
|
3284
|
-
|
|
3285
|
-
# Get user prompt about the image
|
|
3286
|
-
user_prompt = input(
|
|
3287
|
-
"Enter a prompt for the LLM about this image (or press Enter to skip): "
|
|
3288
|
-
)
|
|
3289
|
-
|
|
3290
|
-
# Read image file as binary data
|
|
3291
|
-
try:
|
|
3292
|
-
with open(file_path, "rb") as img_file:
|
|
3293
|
-
img_data = img_file.read()
|
|
3294
|
-
|
|
3295
|
-
# Create an attachment for the image
|
|
3296
|
-
image_attachment = {
|
|
3297
|
-
"name": filename,
|
|
3298
|
-
"type": guess_mime_type(filename),
|
|
3299
|
-
"data": img_data,
|
|
3300
|
-
"size": len(img_data),
|
|
3301
|
-
}
|
|
3302
|
-
|
|
3303
|
-
# Save user message with image attachment
|
|
3304
|
-
message_id = save_conversation_message(
|
|
3305
|
-
command_history,
|
|
3306
|
-
conversation_id,
|
|
3307
|
-
"user",
|
|
3308
|
-
(
|
|
3309
|
-
user_prompt
|
|
3310
|
-
if user_prompt
|
|
3311
|
-
else f"Please analyze this image: {filename}"
|
|
3312
|
-
),
|
|
3313
|
-
wd=os.getcwd(),
|
|
3314
|
-
model=model,
|
|
3315
|
-
provider=provider,
|
|
3316
|
-
npc=npc.name if npc else None,
|
|
3317
|
-
attachments=[image_attachment],
|
|
3318
|
-
)
|
|
3319
|
-
|
|
3320
|
-
# Now use analyze_image which will process the image
|
|
3321
|
-
output = analyze_image(
|
|
3322
|
-
command_history,
|
|
3323
|
-
user_prompt,
|
|
3324
|
-
file_path,
|
|
3325
|
-
filename,
|
|
3326
|
-
npc=npc,
|
|
3327
|
-
stream=stream,
|
|
3328
|
-
message_id=message_id, # Pass the message ID for reference
|
|
3329
|
-
)
|
|
3330
|
-
|
|
3331
|
-
# Save assistant's response
|
|
3332
|
-
if output and isinstance(output, str):
|
|
3333
|
-
save_conversation_message(
|
|
3334
|
-
command_history,
|
|
3335
|
-
conversation_id,
|
|
3336
|
-
"assistant",
|
|
3337
|
-
output,
|
|
3338
|
-
wd=os.getcwd(),
|
|
3339
|
-
model=model,
|
|
3340
|
-
provider=provider,
|
|
3341
|
-
npc=npc.name if npc else None,
|
|
3342
|
-
)
|
|
3343
|
-
|
|
3344
|
-
# Update spool context with this exchange
|
|
3345
|
-
spool_context.append(
|
|
3346
|
-
{"role": "user", "content": user_prompt, "image": file_path}
|
|
3347
|
-
)
|
|
3348
|
-
spool_context.append({"role": "assistant", "content": output})
|
|
3349
|
-
|
|
3350
|
-
if isinstance(output, dict) and "filename" in output:
|
|
3351
|
-
message = f"Screenshot captured: {output['filename']}\nFull path: {output['file_path']}\nLLM-ready data available."
|
|
3352
|
-
else:
|
|
3353
|
-
message = output
|
|
3354
|
-
|
|
3355
|
-
render_markdown(
|
|
3356
|
-
output["response"]
|
|
3357
|
-
if isinstance(output["response"], str)
|
|
3358
|
-
else str(output["response"])
|
|
3359
|
-
)
|
|
3360
|
-
continue
|
|
3361
|
-
|
|
3362
|
-
except Exception as e:
|
|
3363
|
-
print(f"Error processing image: {str(e)}")
|
|
3364
|
-
continue
|
|
3365
|
-
|
|
3366
|
-
# Prepare kwargs for get_conversation
|
|
3367
|
-
kwargs_to_pass = {}
|
|
3368
|
-
if npc:
|
|
3369
|
-
kwargs_to_pass["npc"] = npc
|
|
3370
|
-
if npc.model:
|
|
3371
|
-
kwargs_to_pass["model"] = npc.model
|
|
3372
|
-
|
|
3373
|
-
if npc.provider:
|
|
3374
|
-
kwargs_to_pass["provider"] = npc.provider
|
|
3375
|
-
|
|
3376
|
-
# Incorporate the loaded content into the prompt for conversation
|
|
3377
|
-
if loaded_content:
|
|
3378
|
-
context_content = ""
|
|
3379
|
-
for filename, content in loaded_content.items():
|
|
3380
|
-
# now do a rag search with the loaded_content
|
|
3381
|
-
retrieved_docs = rag_search(
|
|
3382
|
-
user_input,
|
|
3383
|
-
content,
|
|
3384
|
-
similarity_threshold=rag_similarity_threshold,
|
|
3385
|
-
device=device,
|
|
3386
|
-
)
|
|
3387
|
-
if retrieved_docs:
|
|
3388
|
-
context_content += (
|
|
3389
|
-
f"\n\nLoaded content from: {filename}\n{content}\n\n"
|
|
3390
|
-
)
|
|
3391
|
-
if len(context_content) > 0:
|
|
3392
|
-
user_input += f"""
|
|
3393
|
-
Here is the loaded content that may be relevant to your query:
|
|
3394
|
-
{context_content}
|
|
3395
|
-
Please reference it explicitly in your response and use it for answering.
|
|
3396
|
-
"""
|
|
3397
|
-
|
|
3398
|
-
# Add user input to spool context
|
|
3399
|
-
spool_context.append({"role": "user", "content": user_input})
|
|
3400
|
-
|
|
3401
|
-
# Save user message to conversation history
|
|
3402
|
-
message_id = save_conversation_message(
|
|
3403
|
-
command_history,
|
|
3404
|
-
conversation_id,
|
|
3405
|
-
"user",
|
|
3406
|
-
user_input,
|
|
3407
|
-
wd=os.getcwd(),
|
|
3408
|
-
model=model,
|
|
3409
|
-
provider=provider,
|
|
3410
|
-
npc=npc.name if npc else None,
|
|
3411
|
-
)
|
|
3412
|
-
|
|
3413
|
-
# Get the conversation
|
|
3414
|
-
if stream:
|
|
3415
|
-
conversation_result = ""
|
|
3416
|
-
output = get_stream(spool_context, **kwargs_to_pass)
|
|
3417
|
-
for chunk in output:
|
|
3418
|
-
if provider == "anthropic":
|
|
3419
|
-
if chunk.type == "content_block_delta":
|
|
3420
|
-
chunk_content = chunk.delta.text
|
|
3421
|
-
if chunk_content:
|
|
3422
|
-
conversation_result += chunk_content
|
|
3423
|
-
print(chunk_content, end="")
|
|
3424
|
-
|
|
3425
|
-
elif (
|
|
3426
|
-
provider == "openai"
|
|
3427
|
-
or provider == "deepseek"
|
|
3428
|
-
or provider == "openai-like"
|
|
3429
|
-
):
|
|
3430
|
-
chunk_content = "".join(
|
|
3431
|
-
choice.delta.content
|
|
3432
|
-
for choice in chunk.choices
|
|
3433
|
-
if choice.delta.content is not None
|
|
3434
|
-
)
|
|
3435
|
-
if chunk_content:
|
|
3436
|
-
conversation_result += chunk_content
|
|
3437
|
-
print(chunk_content, end="")
|
|
3438
|
-
|
|
3439
|
-
elif provider == "ollama":
|
|
3440
|
-
chunk_content = chunk["message"]["content"]
|
|
3441
|
-
if chunk_content:
|
|
3442
|
-
conversation_result += chunk_content
|
|
3443
|
-
print(chunk_content, end="")
|
|
3444
|
-
print("\n")
|
|
3445
|
-
conversation_result = spool_context + [
|
|
3446
|
-
{"role": "assistant", "content": conversation_result}
|
|
3447
|
-
]
|
|
3448
|
-
else:
|
|
3449
|
-
conversation_result = get_conversation(spool_context, **kwargs_to_pass)
|
|
3450
|
-
|
|
3451
|
-
# Handle potential errors in conversation_result
|
|
3452
|
-
if isinstance(conversation_result, str) and "Error" in conversation_result:
|
|
3453
|
-
print(conversation_result) # Print the error message
|
|
3454
|
-
continue # Skip to the next loop iteration
|
|
3455
|
-
elif (
|
|
3456
|
-
not isinstance(conversation_result, list)
|
|
3457
|
-
or len(conversation_result) == 0
|
|
3458
|
-
):
|
|
3459
|
-
print("Error: Invalid response from get_conversation")
|
|
3460
|
-
continue
|
|
3461
|
-
|
|
3462
|
-
spool_context = conversation_result # update spool_context
|
|
3463
|
-
|
|
3464
|
-
# Extract assistant's reply, handling potential KeyError
|
|
3465
|
-
try:
|
|
3466
|
-
# print(spool_context[-1])
|
|
3467
|
-
# print(provider)
|
|
3468
|
-
if provider == "gemini":
|
|
3469
|
-
assistant_reply = spool_context[-1]["parts"][0]
|
|
3470
|
-
else:
|
|
3471
|
-
assistant_reply = spool_context[-1]["content"]
|
|
3472
|
-
|
|
3473
|
-
except (KeyError, IndexError) as e:
|
|
3474
|
-
print(f"Error extracting assistant's reply: {e}")
|
|
3475
|
-
print(spool_context[-1])
|
|
3476
|
-
print(
|
|
3477
|
-
f"Conversation result: {conversation_result}"
|
|
3478
|
-
) # Print for debugging
|
|
3479
|
-
continue
|
|
3480
|
-
|
|
3481
|
-
# Save assistant's response to conversation history
|
|
3482
|
-
save_conversation_message(
|
|
3483
|
-
command_history,
|
|
3484
|
-
conversation_id,
|
|
3485
|
-
"assistant",
|
|
3486
|
-
assistant_reply,
|
|
3487
|
-
wd=os.getcwd(),
|
|
3488
|
-
model=model,
|
|
3489
|
-
provider=provider,
|
|
3490
|
-
npc=npc.name if npc else None,
|
|
3491
|
-
)
|
|
3492
|
-
|
|
3493
|
-
# sometimes claude responds with unfinished markdown notation. so we need to check if there are two sets
|
|
3494
|
-
# of markdown notation and if not, we add it. so if # markdown notations is odd we add one more
|
|
3495
|
-
if assistant_reply.count("```") % 2 != 0:
|
|
3496
|
-
assistant_reply = assistant_reply + "```"
|
|
3497
|
-
|
|
3498
|
-
if not stream:
|
|
3499
|
-
render_markdown(assistant_reply)
|
|
3500
|
-
|
|
3501
|
-
except (KeyboardInterrupt, EOFError):
|
|
3502
|
-
print("\nExiting spool mode.")
|
|
3503
|
-
break
|
|
3504
|
-
|
|
3505
|
-
return {
|
|
3506
|
-
"messages": spool_context,
|
|
3507
|
-
"output": "\n".join(
|
|
3508
|
-
[msg["content"] for msg in spool_context if msg["role"] == "assistant"]
|
|
3509
|
-
),
|
|
3510
|
-
}
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
def guess_mime_type(filename):
|
|
3514
|
-
"""Guess the MIME type of a file based on its extension."""
|
|
3515
|
-
extension = os.path.splitext(filename)[1].lower()
|
|
3516
|
-
mime_types = {
|
|
3517
|
-
".jpg": "image/jpeg",
|
|
3518
|
-
".jpeg": "image/jpeg",
|
|
3519
|
-
".png": "image/png",
|
|
3520
|
-
".gif": "image/gif",
|
|
3521
|
-
".bmp": "image/bmp",
|
|
3522
|
-
".webp": "image/webp",
|
|
3523
|
-
".pdf": "application/pdf",
|
|
3524
|
-
".txt": "text/plain",
|
|
3525
|
-
".csv": "text/csv",
|
|
3526
|
-
".json": "application/json",
|
|
3527
|
-
".md": "text/markdown",
|
|
3528
|
-
}
|
|
3529
|
-
return mime_types.get(extension, "application/octet-stream")
|