npcsh 0.3.31__py3-none-any.whl → 1.0.0__py3-none-any.whl

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