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