npcsh 1.1.3__py3-none-any.whl → 1.1.5__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 (106) hide show
  1. npcsh/_state.py +48 -64
  2. npcsh/npc_team/corca_example.png +0 -0
  3. npcsh/npc_team/jinxs/{python_executor.jinx → code/python.jinx} +1 -1
  4. npcsh/npc_team/jinxs/{bash_executer.jinx → code/sh.jinx} +1 -1
  5. npcsh/npc_team/jinxs/code/sql.jinx +18 -0
  6. npcsh/npc_team/jinxs/modes/alicanto.jinx +88 -0
  7. npcsh/npc_team/jinxs/modes/corca.jinx +28 -0
  8. npcsh/npc_team/jinxs/modes/guac.jinx +46 -0
  9. npcsh/npc_team/jinxs/modes/plonk.jinx +57 -0
  10. npcsh/npc_team/jinxs/modes/pti.jinx +28 -0
  11. npcsh/npc_team/jinxs/modes/spool.jinx +40 -0
  12. npcsh/npc_team/jinxs/modes/wander.jinx +81 -0
  13. npcsh/npc_team/jinxs/modes/yap.jinx +25 -0
  14. npcsh/npc_team/jinxs/utils/breathe.jinx +20 -0
  15. npcsh/npc_team/jinxs/utils/core/build.jinx +65 -0
  16. npcsh/npc_team/jinxs/utils/core/compile.jinx +50 -0
  17. npcsh/npc_team/jinxs/utils/core/help.jinx +52 -0
  18. npcsh/npc_team/jinxs/utils/core/init.jinx +41 -0
  19. npcsh/npc_team/jinxs/utils/core/jinxs.jinx +32 -0
  20. npcsh/npc_team/jinxs/utils/core/set.jinx +40 -0
  21. npcsh/npc_team/jinxs/{edit_file.jinx → utils/edit_file.jinx} +1 -1
  22. npcsh/npc_team/jinxs/utils/flush.jinx +39 -0
  23. npcsh/npc_team/jinxs/utils/npc-studio.jinx +82 -0
  24. npcsh/npc_team/jinxs/utils/ots.jinx +92 -0
  25. npcsh/npc_team/jinxs/utils/plan.jinx +33 -0
  26. npcsh/npc_team/jinxs/utils/roll.jinx +66 -0
  27. npcsh/npc_team/jinxs/utils/sample.jinx +56 -0
  28. npcsh/npc_team/jinxs/utils/search/brainblast.jinx +51 -0
  29. npcsh/npc_team/jinxs/utils/search/rag.jinx +70 -0
  30. npcsh/npc_team/jinxs/utils/search/search.jinx +192 -0
  31. npcsh/npc_team/jinxs/utils/serve.jinx +29 -0
  32. npcsh/npc_team/jinxs/utils/sleep.jinx +116 -0
  33. npcsh/npc_team/jinxs/utils/trigger.jinx +36 -0
  34. npcsh/npc_team/jinxs/utils/vixynt.jinx +129 -0
  35. npcsh/npcsh.py +14 -12
  36. npcsh/routes.py +80 -1420
  37. npcsh-1.1.5.data/data/npcsh/npc_team/alicanto.jinx +88 -0
  38. npcsh-1.1.5.data/data/npcsh/npc_team/brainblast.jinx +51 -0
  39. npcsh-1.1.5.data/data/npcsh/npc_team/breathe.jinx +20 -0
  40. npcsh-1.1.5.data/data/npcsh/npc_team/build.jinx +65 -0
  41. npcsh-1.1.5.data/data/npcsh/npc_team/compile.jinx +50 -0
  42. npcsh-1.1.5.data/data/npcsh/npc_team/corca.jinx +28 -0
  43. npcsh-1.1.5.data/data/npcsh/npc_team/corca_example.png +0 -0
  44. {npcsh-1.1.3.data → npcsh-1.1.5.data}/data/npcsh/npc_team/edit_file.jinx +1 -1
  45. npcsh-1.1.5.data/data/npcsh/npc_team/flush.jinx +39 -0
  46. npcsh-1.1.5.data/data/npcsh/npc_team/guac.jinx +46 -0
  47. npcsh-1.1.5.data/data/npcsh/npc_team/help.jinx +52 -0
  48. npcsh-1.1.5.data/data/npcsh/npc_team/init.jinx +41 -0
  49. npcsh-1.1.5.data/data/npcsh/npc_team/jinxs.jinx +32 -0
  50. npcsh-1.1.5.data/data/npcsh/npc_team/npc-studio.jinx +82 -0
  51. npcsh-1.1.5.data/data/npcsh/npc_team/ots.jinx +92 -0
  52. npcsh-1.1.5.data/data/npcsh/npc_team/plan.jinx +33 -0
  53. npcsh-1.1.5.data/data/npcsh/npc_team/plonk.jinx +57 -0
  54. npcsh-1.1.5.data/data/npcsh/npc_team/pti.jinx +28 -0
  55. npcsh-1.1.3.data/data/npcsh/npc_team/python_executor.jinx → npcsh-1.1.5.data/data/npcsh/npc_team/python.jinx +1 -1
  56. npcsh-1.1.5.data/data/npcsh/npc_team/rag.jinx +70 -0
  57. npcsh-1.1.5.data/data/npcsh/npc_team/roll.jinx +66 -0
  58. npcsh-1.1.5.data/data/npcsh/npc_team/sample.jinx +56 -0
  59. npcsh-1.1.5.data/data/npcsh/npc_team/search.jinx +192 -0
  60. npcsh-1.1.5.data/data/npcsh/npc_team/serve.jinx +29 -0
  61. npcsh-1.1.5.data/data/npcsh/npc_team/set.jinx +40 -0
  62. npcsh-1.1.3.data/data/npcsh/npc_team/bash_executer.jinx → npcsh-1.1.5.data/data/npcsh/npc_team/sh.jinx +1 -1
  63. npcsh-1.1.5.data/data/npcsh/npc_team/sleep.jinx +116 -0
  64. npcsh-1.1.5.data/data/npcsh/npc_team/spool.jinx +40 -0
  65. npcsh-1.1.5.data/data/npcsh/npc_team/sql.jinx +18 -0
  66. npcsh-1.1.5.data/data/npcsh/npc_team/trigger.jinx +36 -0
  67. npcsh-1.1.5.data/data/npcsh/npc_team/vixynt.jinx +129 -0
  68. npcsh-1.1.5.data/data/npcsh/npc_team/wander.jinx +81 -0
  69. npcsh-1.1.5.data/data/npcsh/npc_team/yap.jinx +25 -0
  70. {npcsh-1.1.3.dist-info → npcsh-1.1.5.dist-info}/METADATA +1 -1
  71. npcsh-1.1.5.dist-info/RECORD +132 -0
  72. npcsh/npc_team/jinxs/image_generation.jinx +0 -29
  73. npcsh/npc_team/jinxs/internet_search.jinx +0 -31
  74. npcsh/npc_team/jinxs/screen_cap.jinx +0 -25
  75. npcsh-1.1.3.data/data/npcsh/npc_team/image_generation.jinx +0 -29
  76. npcsh-1.1.3.data/data/npcsh/npc_team/internet_search.jinx +0 -31
  77. npcsh-1.1.3.data/data/npcsh/npc_team/screen_cap.jinx +0 -25
  78. npcsh-1.1.3.dist-info/RECORD +0 -78
  79. /npcsh/npc_team/jinxs/{kg_search.jinx → utils/search/kg_search.jinx} +0 -0
  80. /npcsh/npc_team/jinxs/{memory_search.jinx → utils/search/memory_search.jinx} +0 -0
  81. {npcsh-1.1.3.data → npcsh-1.1.5.data}/data/npcsh/npc_team/alicanto.npc +0 -0
  82. {npcsh-1.1.3.data → npcsh-1.1.5.data}/data/npcsh/npc_team/alicanto.png +0 -0
  83. {npcsh-1.1.3.data → npcsh-1.1.5.data}/data/npcsh/npc_team/corca.npc +0 -0
  84. {npcsh-1.1.3.data → npcsh-1.1.5.data}/data/npcsh/npc_team/corca.png +0 -0
  85. {npcsh-1.1.3.data → npcsh-1.1.5.data}/data/npcsh/npc_team/foreman.npc +0 -0
  86. {npcsh-1.1.3.data → npcsh-1.1.5.data}/data/npcsh/npc_team/frederic.npc +0 -0
  87. {npcsh-1.1.3.data → npcsh-1.1.5.data}/data/npcsh/npc_team/frederic4.png +0 -0
  88. {npcsh-1.1.3.data → npcsh-1.1.5.data}/data/npcsh/npc_team/guac.png +0 -0
  89. {npcsh-1.1.3.data → npcsh-1.1.5.data}/data/npcsh/npc_team/kadiefa.npc +0 -0
  90. {npcsh-1.1.3.data → npcsh-1.1.5.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  91. {npcsh-1.1.3.data → npcsh-1.1.5.data}/data/npcsh/npc_team/kg_search.jinx +0 -0
  92. {npcsh-1.1.3.data → npcsh-1.1.5.data}/data/npcsh/npc_team/memory_search.jinx +0 -0
  93. {npcsh-1.1.3.data → npcsh-1.1.5.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
  94. {npcsh-1.1.3.data → npcsh-1.1.5.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  95. {npcsh-1.1.3.data → npcsh-1.1.5.data}/data/npcsh/npc_team/plonk.npc +0 -0
  96. {npcsh-1.1.3.data → npcsh-1.1.5.data}/data/npcsh/npc_team/plonk.png +0 -0
  97. {npcsh-1.1.3.data → npcsh-1.1.5.data}/data/npcsh/npc_team/plonkjr.npc +0 -0
  98. {npcsh-1.1.3.data → npcsh-1.1.5.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  99. {npcsh-1.1.3.data → npcsh-1.1.5.data}/data/npcsh/npc_team/sibiji.npc +0 -0
  100. {npcsh-1.1.3.data → npcsh-1.1.5.data}/data/npcsh/npc_team/sibiji.png +0 -0
  101. {npcsh-1.1.3.data → npcsh-1.1.5.data}/data/npcsh/npc_team/spool.png +0 -0
  102. {npcsh-1.1.3.data → npcsh-1.1.5.data}/data/npcsh/npc_team/yap.png +0 -0
  103. {npcsh-1.1.3.dist-info → npcsh-1.1.5.dist-info}/WHEEL +0 -0
  104. {npcsh-1.1.3.dist-info → npcsh-1.1.5.dist-info}/entry_points.txt +0 -0
  105. {npcsh-1.1.3.dist-info → npcsh-1.1.5.dist-info}/licenses/LICENSE +0 -0
  106. {npcsh-1.1.3.dist-info → npcsh-1.1.5.dist-info}/top_level.txt +0 -0
npcsh/routes.py CHANGED
@@ -1,83 +1,17 @@
1
-
2
-
3
- from typing import Callable, Dict, Any, List, Optional, Union
1
+ from typing import Callable, Dict, Any, List, Optional
4
2
  import functools
5
3
  import os
6
- import subprocess
7
- import sys
8
- from pathlib import Path
9
-
10
4
  import traceback
11
- import shlex
12
- import time
13
- from datetime import datetime
14
- from sqlalchemy import create_engine
15
- import logging
16
- import json
17
- from npcpy.data.load import load_file_contents
18
-
19
- from npcpy.llm_funcs import (
20
- get_llm_response,
21
- gen_image,
22
- gen_video,
23
- breathe,
24
- )
25
- from npcpy.npc_compiler import initialize_npc_project
26
- from npcpy.npc_sysenv import render_markdown
27
- from npcpy.work.plan import execute_plan_command
28
- from npcpy.work.trigger import execute_trigger_command
29
- from npcpy.work.desktop import perform_action
30
- from npcpy.memory.search import execute_rag_command, execute_search_command, execute_brainblast_command
31
- from npcpy.memory.command_history import CommandHistory, load_kg_from_db, save_kg_to_db
32
- from npcpy.serve import start_flask_server
33
- from npcpy.mix.debate import run_debate
34
- from npcpy.data.image import capture_screenshot
35
- from npcpy.npc_compiler import NPC, Team, Jinx,initialize_npc_project
36
- from npcpy.data.web import search_web
37
- from npcpy.memory.knowledge_graph import kg_sleep_process, kg_dream_process
38
-
39
-
40
- from npcsh._state import (
41
- NPCSH_VISION_MODEL,
42
- NPCSH_VISION_PROVIDER,
43
- set_npcsh_config_value,
44
- NPCSH_API_URL,
45
- NPCSH_CHAT_MODEL,
46
- NPCSH_CHAT_PROVIDER,
47
- NPCSH_STREAM_OUTPUT,
48
- NPCSH_IMAGE_GEN_MODEL,
49
- NPCSH_IMAGE_GEN_PROVIDER,
50
- NPCSH_VIDEO_GEN_MODEL,
51
- NPCSH_VIDEO_GEN_PROVIDER,
52
- NPCSH_EMBEDDING_MODEL,
53
- NPCSH_EMBEDDING_PROVIDER,
54
- NPCSH_REASONING_MODEL,
55
- NPCSH_REASONING_PROVIDER,
56
- NPCSH_SEARCH_PROVIDER,
57
- CANONICAL_ARGS,
58
- normalize_and_expand_flags,
59
- get_argument_help,
60
- get_relevant_memories
61
-
62
- )
63
- from npcsh.corca import enter_corca_mode
64
- from npcsh.guac import enter_guac_mode
65
- from npcsh.plonk import execute_plonk_command, format_plonk_summary
66
- from npcsh.alicanto import alicanto
67
- from npcsh.pti import enter_pti_mode
68
- from npcsh.spool import enter_spool_mode
69
- from npcsh.wander import enter_wander_mode
70
- from npcsh.yap import enter_yap_mode
71
-
72
-
5
+ from pathlib import Path
73
6
 
74
- NPC_STUDIO_DIR = Path.home() / ".npcsh" / "npc-studio"
7
+ from npcpy.npc_compiler import Jinx, load_jinxs_from_directory
75
8
 
76
9
 
77
10
  class CommandRouter:
78
11
  def __init__(self):
79
12
  self.routes = {}
80
13
  self.help_info = {}
14
+ self.jinx_routes = {}
81
15
 
82
16
  def route(self, command: str, help_text: str = "") -> Callable:
83
17
  def wrapper(func):
@@ -91,8 +25,80 @@ class CommandRouter:
91
25
  return wrapped_func
92
26
  return wrapper
93
27
 
28
+ def load_jinx_routes(self, jinxs_dir: str):
29
+ if not os.path.exists(jinxs_dir):
30
+ print(f"Jinxs directory not found: {jinxs_dir}")
31
+ return
32
+
33
+ for root, dirs, files in os.walk(jinxs_dir):
34
+ for filename in files:
35
+ if filename.endswith('.jinx'):
36
+ jinx_path = os.path.join(root, filename)
37
+ try:
38
+ jinx = Jinx(jinx_path=jinx_path)
39
+ self.register_jinx(jinx)
40
+ except Exception as e:
41
+ print(f"Error loading jinx {filename}: {e}")
42
+
43
+ def register_jinx(self, jinx: Jinx):
44
+ command_name = jinx.jinx_name
45
+
46
+ def jinx_handler(command: str, **kwargs):
47
+ return self._execute_jinx(jinx, command, **kwargs)
48
+
49
+ self.jinx_routes[command_name] = jinx_handler
50
+ self.help_info[command_name] = jinx.description or "Jinx command"
51
+
52
+ def _execute_jinx(self, jinx: Jinx, command: str, **kwargs):
53
+ messages = kwargs.get("messages", [])
54
+ npc = kwargs.get('npc')
55
+
56
+ try:
57
+ import shlex
58
+ parts = shlex.split(command)
59
+ args = parts[1:] if len(parts) > 1 else []
60
+
61
+ input_values = {}
62
+ if hasattr(jinx, 'inputs') and jinx.inputs:
63
+ for i, input_spec in enumerate(jinx.inputs):
64
+ if isinstance(input_spec, str):
65
+ input_name = input_spec
66
+ elif isinstance(input_spec, dict):
67
+ input_name = list(input_spec.keys())[0]
68
+ else:
69
+ continue
70
+
71
+ if i < len(args):
72
+ input_values[input_name] = args[i]
73
+
74
+ jinx_output = jinx.execute(
75
+ input_values=input_values,
76
+ jinxs_dict=kwargs.get('jinxs_dict', {}),
77
+ npc=npc,
78
+ messages=messages
79
+ )
80
+
81
+ if isinstance(jinx_output, dict):
82
+ return {
83
+ "output": jinx_output.get('output', str(jinx_output)),
84
+ "messages": jinx_output.get('messages', messages)
85
+ }
86
+ else:
87
+ return {"output": str(jinx_output), "messages": messages}
88
+
89
+ except Exception as e:
90
+ traceback.print_exc()
91
+ return {
92
+ "output": f"Error executing jinx '{jinx.jinx_name}': {e}",
93
+ "messages": messages
94
+ }
95
+
94
96
  def get_route(self, command: str) -> Optional[Callable]:
95
- return self.routes.get(command)
97
+ if command in self.routes:
98
+ return self.routes[command]
99
+ elif command in self.jinx_routes:
100
+ return self.jinx_routes[command]
101
+ return None
96
102
 
97
103
  def execute(self, command_str: str, **kwargs) -> Any:
98
104
  command_name = command_str.split()[0].lstrip('/')
@@ -102,7 +108,8 @@ class CommandRouter:
102
108
  return None
103
109
 
104
110
  def get_commands(self) -> List[str]:
105
- return list(self.routes.keys())
111
+ all_commands = list(self.routes.keys()) + list(self.jinx_routes.keys())
112
+ return sorted(set(all_commands))
106
113
 
107
114
  def get_help(self, command: str = None) -> Dict[str, str]:
108
115
  if command:
@@ -111,1352 +118,5 @@ class CommandRouter:
111
118
  return {}
112
119
  return self.help_info
113
120
 
114
- router = CommandRouter()
115
- def get_help_text():
116
- commands = router.get_commands()
117
- help_info = router.help_info
118
-
119
- commands.sort()
120
- output = "# Available Commands\n\n"
121
- for cmd in commands:
122
- help_text = help_info.get(cmd, "")
123
- output += f"/{cmd} - {help_text}\n\n"
124
-
125
- arg_help_map = get_argument_help()
126
- if arg_help_map:
127
- output += "## Common Command-Line Flags\n\n"
128
- output += "The shortest unambiguous prefix works (e.g., `-t` for `--temperature`).\n\n"
129
-
130
-
131
- output += "```\n"
132
-
133
- all_args_to_show = CANONICAL_ARGS[:]
134
- all_args_to_show.sort()
135
-
136
-
137
- NUM_COLUMNS = 4
138
- FLAG_WIDTH = 18
139
- ALIAS_WIDTH = 12
140
- COLUMN_SEPARATOR = " | "
141
-
142
- rows_per_column = (len(all_args_to_show) + NUM_COLUMNS - 1) // NUM_COLUMNS
143
- columns = [all_args_to_show[i:i + rows_per_column] for i in range(0, len(all_args_to_show), rows_per_column)]
144
-
145
- def get_shortest_alias(arg):
146
- if arg in arg_help_map and arg_help_map[arg]:
147
- return min(arg_help_map[arg], key=len)
148
- return ""
149
-
150
- header_parts = []
151
- for _ in range(NUM_COLUMNS):
152
- flag_header = "Flag".ljust(FLAG_WIDTH)
153
- alias_header = "Shorthand".ljust(ALIAS_WIDTH)
154
- header_parts.append(f"{flag_header}{alias_header}")
155
- output += COLUMN_SEPARATOR.join(header_parts) + "\n"
156
-
157
- divider_parts = []
158
- for _ in range(NUM_COLUMNS):
159
-
160
- divider_part = "-" * (FLAG_WIDTH + ALIAS_WIDTH)
161
- divider_parts.append(divider_part)
162
- output += COLUMN_SEPARATOR.join(divider_parts) + "\n"
163
-
164
-
165
- for i in range(rows_per_column):
166
- row_parts = []
167
- for col_idx in range(NUM_COLUMNS):
168
- if col_idx < len(columns) and i < len(columns[col_idx]):
169
- arg = columns[col_idx][i]
170
- alias = get_shortest_alias(arg)
171
- alias_display = f"(-{alias})" if alias else ""
172
-
173
- flag_part = f"--{arg}".ljust(FLAG_WIDTH)
174
- alias_part = alias_display.ljust(ALIAS_WIDTH)
175
- row_parts.append(f"{flag_part}{alias_part}")
176
- else:
177
-
178
- row_parts.append(" " * (FLAG_WIDTH + ALIAS_WIDTH))
179
-
180
- output += COLUMN_SEPARATOR.join(row_parts) + "\n"
181
-
182
-
183
- output += "```\n"
184
-
185
- output += """
186
- \n## Note
187
- - Bash commands and programs can be executed directly (try bash first, then LLM).
188
- - Use '/exit' or '/quit' to exit the current NPC mode or the npcsh shell.
189
- - Jinxs defined for the current NPC or Team can also be used like commands (e.g., /screenshot).
190
- """
191
- return output
192
- def safe_get(kwargs, key, default=None):
193
- return kwargs.get(key, default)
194
-
195
-
196
- @router.route("build", "Build deployment artifacts for NPC team")
197
- def build_handler(command: str, **kwargs):
198
- parts = shlex.split(command)
199
-
200
- target = safe_get(kwargs, 'target', 'flask')
201
- output_dir = safe_get(kwargs, 'output', './build')
202
- team_path = safe_get(kwargs, 'team', './npc_team')
203
-
204
- if len(parts) > 1:
205
- target = parts[1]
206
-
207
- build_config = {
208
- 'team_path': os.path.abspath(team_path),
209
- 'output_dir': os.path.abspath(output_dir),
210
- 'target': target,
211
- 'port': safe_get(kwargs, 'port', 5337),
212
- 'cors_origins': safe_get(kwargs, 'cors', None),
213
- }
214
-
215
- builders = {
216
- 'flask': build_flask_server,
217
- 'docker': build_docker_compose,
218
- 'cli': build_cli_executable,
219
- 'static': build_static_site,
220
- }
221
-
222
- if target not in builders:
223
- return {
224
- "output": f"Unknown target: {target}. Available: {list(builders.keys())}",
225
- "messages": kwargs.get('messages', [])
226
- }
227
-
228
- return builders[target](build_config, **kwargs)
229
-
230
- @router.route("breathe", "Condense context on a regular cadence")
231
- def breathe_handler(command: str, **kwargs):
232
-
233
- result = breathe(**kwargs)
234
- if isinstance(result, dict):
235
- return result
236
-
237
-
238
-
239
-
240
-
241
-
242
- @router.route("compile", "Compile NPC profiles")
243
- def compile_handler(command: str, **kwargs):
244
- messages = safe_get(kwargs, "messages", [])
245
- npc_team_dir = safe_get(kwargs, 'current_path', './npc_team')
246
- parts = command.split()
247
- npc_file_path_arg = parts[1] if len(parts) > 1 else None
248
- output = ""
249
- try:
250
- if npc_file_path_arg:
251
- npc_full_path = os.path.abspath(npc_file_path_arg)
252
- if os.path.exists(npc_full_path):
253
- npc = NPC(npc_full_path)
254
- output = f"Compiled NPC: {npc_full_path}"
255
- else:
256
- output = f"Error: NPC file not found: {npc_full_path}"
257
- else:
258
- npc = NPC(npc_full_path)
259
-
260
- output = f"Compiled all NPCs in directory: {npc_team_dir}"
261
- except NameError:
262
- output = "Compile functions (compile_npc_file, compile_team_npcs) not available."
263
- except Exception as e:
264
- traceback.print_exc()
265
- output = f"Error compiling: {e}"
266
- return {"output": output, "messages": messages, "npc": npc}
267
-
268
-
269
-
270
- @router.route("corca", "Enter the Corca MCP-powered agentic shell. Usage: /corca [--mcp-server-path path]")
271
- def corca_handler(command: str, **kwargs):
272
- from npcsh._state import initial_state, setup_shell
273
- command_history, team, default_npc = setup_shell()
274
-
275
-
276
- return enter_corca_mode(command=command,
277
- command_history = command_history,
278
- shell_state=initial_state)
279
-
280
- @router.route("flush", "Flush the last N messages")
281
- def flush_handler(command: str, **kwargs):
282
- messages = safe_get(kwargs, "messages", [])
283
- try:
284
- parts = command.split()
285
- n = int(parts[1]) if len(parts) > 1 else 1
286
- except (ValueError, IndexError):
287
- return {"output": "Usage: /flush [number_of_messages_to_flush]", "messages": messages}
288
-
289
- if n <= 0:
290
- return {"output": "Error: Number of messages must be positive.", "messages": messages}
291
-
292
- new_messages = list(messages)
293
- original_len = len(new_messages)
294
- removed_count = 0
295
-
296
- if new_messages and new_messages[0].get("role") == "system":
297
- system_message = new_messages[0]
298
- working_messages = new_messages[1:]
299
- num_to_remove = min(n, len(working_messages))
300
- if num_to_remove > 0:
301
- final_messages = [system_message] + working_messages[:-num_to_remove]
302
- removed_count = num_to_remove
303
- else:
304
- final_messages = [system_message]
305
- else:
306
- num_to_remove = min(n, original_len)
307
- if num_to_remove > 0:
308
- final_messages = new_messages[:-num_to_remove]
309
- removed_count = num_to_remove
310
- else:
311
- final_messages = []
312
-
313
- output = f"Flushed {removed_count} message(s). Context is now {len(final_messages)} messages."
314
- return {"output": output, "messages": final_messages}
315
-
316
- @router.route("guac", "Enter guac mode")
317
- def guac_handler(command, **kwargs):
318
- '''
319
- Guac ignores input npc and npc_team dirs and manually sets them to be at ~/.npcsh/guac/
320
-
321
- '''
322
- config_dir = safe_get(kwargs, 'config_dir', None)
323
- plots_dir = safe_get(kwargs, 'plots_dir', None)
324
- refresh_period = safe_get(kwargs, 'refresh_period', 100)
325
- lang = safe_get(kwargs, 'lang', None)
326
- messages = safe_get(kwargs, "messages", [])
327
- db_conn = safe_get(kwargs, 'db_conn', create_engine('sqlite:///'+os.path.expanduser('~/npcsh_history.db')))
328
-
329
- npc_file = '~/.npcsh/guac/npc_team/guac.npc'
330
- npc_team_dir = os.path.expanduser('~/.npcsh/guac/npc_team/')
331
-
332
- npc = NPC(file=npc_file, db_conn=db_conn)
333
-
334
- team = Team(npc_team_dir, db_conn=db_conn)
335
-
336
-
337
- enter_guac_mode(
338
- npc=npc,
339
- team=team,
340
- config_dir=config_dir,
341
- plots_dir=plots_dir,
342
- npc_team_dir=npc_team_dir,
343
- refresh_period=refresh_period, lang=lang)
344
-
345
- return {"output": 'Exiting Guac Mode', "messages": safe_get(kwargs, "messages", [])}
346
-
347
-
348
- @router.route("help", "Show help for commands, NPCs, or Jinxs. Usage: /help [topic]")
349
- def help_handler(command: str, **kwargs):
350
- messages = safe_get(kwargs, "messages", [])
351
- parts = shlex.split(command)
352
- if len(parts) < 2:
353
- return {"output": get_help_text(), "messages": messages}
354
- target = parts[1].lstrip('/')
355
- output = ""
356
-
357
-
358
-
359
- if target in router.get_commands():
360
- help_text = router.get_help(target).get(target, "No description available.")
361
- output = f"## Help for Command: `/{target}`\n\n- **Description**: {help_text}"
362
- return {"output": output, "messages": messages}
363
-
364
- team = safe_get(kwargs, 'team')
365
- if team and target in team.npcs:
366
- npc_obj = team.npcs[target]
367
- output = f"## Help for NPC: `{target}`\n\n"
368
- output += f"- **Primary Directive**: {npc_obj.primary_directive}\n"
369
- output += f"- **Default Model**: `{npc_obj.model}`\n"
370
- output += f"- **Default Provider**: `{npc_obj.provider}`\n"
371
- if hasattr(npc_obj, 'jinxs_dict') and npc_obj.jinxs_dict:
372
- jinx_names = ", ".join([f"`{j}`" for j in npc_obj.jinxs_dict.keys()])
373
- output += f"- **Associated Jinxs**: {jinx_names}\n"
374
- return {"output": output, "messages": messages}
375
-
376
-
377
- npc = safe_get(kwargs, 'npc')
378
- jinx_obj = None
379
- source = ""
380
- if npc and hasattr(npc, 'jinxs_dict') and target in npc.jinxs_dict:
381
- jinx_obj = npc.jinxs_dict[target]
382
- source = f" (from NPC: `{npc.name}`)"
383
- elif team and hasattr(team, 'jinxs_dict') and target in team.jinxs_dict:
384
- jinx_obj = team.jinxs_dict[target]
385
- source = f" (from Team: `{team.name}`)"
386
-
387
- if jinx_obj:
388
- output = f"## Help for Jinx: `/{target}`{source}\n\n"
389
- output += f"- **Description**: {jinx_obj.description}\n"
390
- if hasattr(jinx_obj, 'inputs') and jinx_obj.inputs:
391
- inputs_str = json.dumps(jinx_obj.inputs, indent=2)
392
- output += f"- **Inputs**:\n```json\n{inputs_str}\n```\n"
393
- return {"output": output, "messages": messages}
394
-
395
-
396
- return {"output": f"Sorry, no help topic found for `{target}`.", "messages": messages}
397
-
398
-
399
-
400
-
401
- @router.route("init", "Initialize NPC project")
402
- def init_handler(command: str, **kwargs):
403
- messages = safe_get(kwargs, "messages", [])
404
- try:
405
- parts = shlex.split(command)
406
- directory = "."
407
- templates = None
408
- context = None
409
-
410
- if len(parts) > 1 and not parts[1].startswith("-"):
411
- directory = parts[1]
412
-
413
-
414
- initialize_npc_project(
415
- directory=directory,
416
- templates=templates,
417
- context=context,
418
- model=safe_get(kwargs, 'model'),
419
- provider=safe_get(kwargs, 'provider')
420
- )
421
- output = f"NPC project initialized in {os.path.abspath(directory)}."
422
- except NameError:
423
- output = "Init function (initialize_npc_project) not available."
424
- except Exception as e:
425
- traceback.print_exc()
426
- output = f"Error initializing project: {e}"
427
- return {"output": output, "messages": messages}
428
-
429
- def ensure_repo():
430
- """Clone or update the npc-studio repo."""
431
- if not NPC_STUDIO_DIR.exists():
432
- os.makedirs(NPC_STUDIO_DIR.parent, exist_ok=True)
433
- subprocess.check_call([
434
- "git", "clone",
435
- "https://github.com/npc-worldwide/npc-studio.git",
436
- str(NPC_STUDIO_DIR)
437
- ])
438
- else:
439
- subprocess.check_call(
440
- ["git", "pull"],
441
- cwd=NPC_STUDIO_DIR
442
- )
443
-
444
- def install_dependencies():
445
- """Install npm and pip dependencies."""
446
-
447
- subprocess.check_call(["npm", "install"], cwd=NPC_STUDIO_DIR)
448
-
449
-
450
- req_file = NPC_STUDIO_DIR / "requirements.txt"
451
- if req_file.exists():
452
- subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", str(req_file)])
453
- def launch_npc_studio(path_to_open: str = None):
454
- """
455
- Launch the NPC Studio backend + frontend.
456
- Returns PIDs for processes.
457
- """
458
- ensure_repo()
459
- install_dependencies()
460
-
461
-
462
- backend = subprocess.Popen(
463
- [sys.executable, "npc_studio_serve.py"],
464
- cwd=NPC_STUDIO_DIR,
465
- shell = False
466
- )
467
-
468
-
469
- dev_server = subprocess.Popen(
470
- ["npm", "run", "dev"],
471
- cwd=NPC_STUDIO_DIR,
472
- shell=False
473
- )
474
-
475
-
476
- frontend = subprocess.Popen(
477
- ["npm", "start"],
478
- cwd=NPC_STUDIO_DIR,
479
- shell=False
480
- )
481
-
482
- return backend, dev_server, frontend
483
-
484
- @router.route("npc-studio", "Start npc studio")
485
- def npc_studio_handler(command: str, **kwargs):
486
- messages = kwargs.get("messages", [])
487
- user_command = " ".join(command.split()[1:])
488
-
489
- try:
490
- backend, electron, frontend = launch_npc_studio(user_command or None)
491
- return {
492
- "output": f"NPC Studio started!\nBackend PID={backend.pid}, Electron PID={electron.pid} Frontend PID={frontend.pid}",
493
- "messages": messages
494
- }
495
- except Exception as e:
496
- return {
497
- "output": f"Failed to start NPC Studio: {e}",
498
- "messages": messages
499
- }
500
- @router.route("ots", "Take screenshot and analyze with vision model")
501
- def ots_handler(command: str, **kwargs):
502
- command_parts = command.split()
503
- image_paths = []
504
- npc = safe_get(kwargs, 'npc')
505
- vision_model = safe_get(kwargs,
506
- 'vmodel',
507
- NPCSH_VISION_MODEL)
508
- vision_provider = safe_get(kwargs,
509
- 'vprovider',
510
- NPCSH_VISION_PROVIDER)
511
- messages = safe_get(kwargs,
512
- 'messages',
513
- [])
514
- stream = safe_get(kwargs,
515
- 'stream',
516
- NPCSH_STREAM_OUTPUT)
517
-
518
- try:
519
- if len(command_parts) > 1:
520
- for img_path_arg in command_parts[1:]:
521
- full_path = os.path.abspath(img_path_arg)
522
- if os.path.exists(full_path):
523
- image_paths.append(full_path)
524
- else:
525
- return {"output": f"Error: Image file not found at {full_path}", "messages": messages}
526
- else:
527
- screenshot_info = capture_screenshot(full=False)
528
- if screenshot_info and "file_path" in screenshot_info:
529
- image_paths.append(screenshot_info["file_path"])
530
- print(f"Screenshot captured: {screenshot_info.get('filename', os.path.basename(screenshot_info['file_path']))}")
531
- else:
532
- return {"output": "Error: Failed to capture screenshot.", "messages": messages}
533
-
534
- if not image_paths:
535
- return {"output": "No valid images found or captured.", "messages": messages}
536
-
537
- user_prompt = safe_get(kwargs, 'stdin_input')
538
- if user_prompt is None:
539
- try:
540
- user_prompt = input(
541
- "Enter a prompt for the LLM about these images (or press Enter to skip): "
542
- )
543
- except EOFError:
544
- user_prompt = "Describe the image(s)."
545
-
546
- if not user_prompt or not user_prompt.strip():
547
- user_prompt = "Describe the image(s)."
548
-
549
- response_data = get_llm_response(
550
- prompt=user_prompt,
551
- model=vision_model,
552
- provider=vision_provider,
553
- messages=messages,
554
- images=image_paths,
555
- stream=stream,
556
- npc=npc,
557
- api_url=safe_get(kwargs, 'api_url'),
558
- api_key=safe_get(kwargs, 'api_key')
559
- )
560
- return {"output": response_data.get('response'), "messages": response_data.get('messages'), "model": vision_model, "provider": vision_provider}
561
-
562
- except Exception as e:
563
- traceback.print_exc()
564
- return {"output": f"Error during /ots command: {e}", "messages": messages}
565
-
566
-
567
-
568
-
569
- @router.route("plan", "Execute a plan command")
570
- def plan_handler(command: str, **kwargs):
571
- messages = safe_get(kwargs, "messages", [])
572
- user_command = " ".join(command.split()[1:])
573
- if not user_command:
574
- return {"output": "Usage: /plan <description_of_plan>", "messages": messages}
575
-
576
- return execute_plan_command(command=user_command, **kwargs)
577
-
578
-
579
-
580
-
581
-
582
-
583
- @router.route("pti", "Enter Pardon-The-Interruption mode for human-in-the-loop reasoning.")
584
- def pti_handler(command: str, **kwargs):
585
- return enter_pti_mode(command=command, **kwargs)
586
-
587
- @router.route("plonk", "Use vision model to interact with GUI. Usage: /plonk <task description>")
588
- def plonk_handler(command: str, **kwargs):
589
- messages = safe_get(kwargs, "messages", [])
590
-
591
-
592
-
593
- positional_args = safe_get(kwargs, 'positional_args', [])
594
- request_str = " ".join(positional_args)
595
-
596
- if not request_str:
597
- return {"output": "Usage: /plonk <task_description> [--vmodel model_name] [--vprovider provider_name]", "messages": messages}
598
-
599
- try:
600
- plonk_context = safe_get(kwargs, 'plonk_context')
601
-
602
-
603
- summary_data = execute_plonk_command(
604
- request=request_str,
605
- model=safe_get(kwargs, 'vmodel', NPCSH_VISION_MODEL),
606
- provider=safe_get(kwargs, 'vprovider', NPCSH_VISION_PROVIDER),
607
- npc=safe_get(kwargs, 'npc'),
608
- plonk_context=plonk_context,
609
- debug=True
610
- )
611
-
612
- if summary_data and isinstance(summary_data, list):
613
- output_report = format_plonk_summary(summary_data)
614
- return {"output": output_report, "messages": messages}
615
- else:
616
- return {"output": "Plonk command did not complete within the maximum number of iterations.", "messages": messages}
617
-
618
- except Exception as e:
619
- traceback.print_exc()
620
- return {"output": f"Error executing plonk command: {e}", "messages": messages}
621
-
622
-
623
- @router.route("brainblast", "Execute an advanced chunked search on command history")
624
- def brainblast_handler(command: str, **kwargs):
625
- messages = safe_get(kwargs, "messages", [])
626
- parts = shlex.split(command)
627
- search_query = " ".join(parts[1:]) if len(parts) > 1 else ""
628
-
629
- if not search_query:
630
- return {"output": "Usage: /brainblast <search_terms>", "messages": messages}
631
-
632
-
633
- command_history = kwargs.get('command_history')
634
- if not command_history:
635
-
636
-
637
- db_path = safe_get(kwargs, "history_db_path", os.path.expanduser('~/npcsh_history.db'))
638
- try:
639
- command_history = CommandHistory(db_path)
640
- kwargs['command_history'] = command_history
641
- except Exception as e:
642
- return {"output": f"Error connecting to command history: {e}", "messages": messages}
643
-
644
- try:
645
-
646
- if 'messages' in kwargs:
647
- del kwargs['messages']
648
-
649
-
650
- return execute_brainblast_command(
651
- command=search_query,
652
- **kwargs)
653
- except Exception as e:
654
- traceback.print_exc()
655
- return {"output": f"Error executing brainblast command: {e}", "messages": messages}
656
-
657
- @router.route("rag", "Execute a RAG command using ChromaDB embeddings with optional file input (-f/--file)")
658
- def rag_handler(command: str, **kwargs):
659
- parts = shlex.split(command)
660
- user_command = []
661
- file_paths = []
662
-
663
- i = 1
664
- while i < len(parts):
665
- if parts[i] == "-f" or parts[i] == "--file":
666
-
667
- if i + 1 < len(parts):
668
- file_paths.append(parts[i + 1])
669
- i += 2
670
- else:
671
- return {"output": "Error: -f/--file flag needs a file path", "messages": messages}
672
- else:
673
-
674
- user_command.append(parts[i])
675
- i += 1
676
-
677
- user_command = " ".join(user_command)
678
-
679
- vector_db_path = safe_get(kwargs, "vector_db_path", os.path.expanduser('~/npcsh_chroma.db'))
680
- embedding_model = safe_get(kwargs, "emodel", NPCSH_EMBEDDING_MODEL)
681
- embedding_provider = safe_get(kwargs, "eprovider", NPCSH_EMBEDDING_PROVIDER)
682
-
683
- if not user_command and not file_paths:
684
- return {"output": "Usage: /rag [-f file_path] <query>", "messages": kwargs.get('messages', [])}
685
-
686
- try:
687
-
688
- file_contents = []
689
- for file_path in file_paths:
690
- try:
691
- chunks = load_file_contents(file_path)
692
- file_name = os.path.basename(file_path)
693
- file_contents.extend([f"[{file_name}] {chunk}" for chunk in chunks])
694
- except Exception as file_err:
695
- file_contents.append(f"Error processing file {file_path}: {str(file_err)}")
696
- exe_rag = execute_rag_command(
697
- command=user_command,
698
- vector_db_path=vector_db_path,
699
- embedding_model=embedding_model,
700
- embedding_provider=embedding_provider,
701
- file_contents=file_contents if file_paths else None,
702
- **kwargs
703
- )
704
- return {'output':exe_rag.get('response'), 'messages': exe_rag.get('messages', kwargs.get('messages', []))}
705
-
706
- except Exception as e:
707
- traceback.print_exc()
708
- return {"output": f"Error executing RAG command: {e}", "messages": kwargs.get('messages', [])}
709
- @router.route("roll", "generate a video")
710
- def roll_handler(command: str, **kwargs):
711
- messages = safe_get(kwargs, "messages", [])
712
- prompt = " ".join(command.split()[1:])
713
- num_frames = safe_get(kwargs, 'num_frames', 125)
714
- width = safe_get(kwargs, 'width', 256)
715
- height = safe_get(kwargs, 'height', 256)
716
- output_path = safe_get(kwargs, 'output_path', "output.mp4")
717
- if not prompt:
718
- return {"output": "Usage: /roll <your prompt>", "messages": messages}
719
- try:
720
- result = gen_video(
721
- prompt=prompt,
722
- model=safe_get(kwargs, 'vgmodel', NPCSH_VIDEO_GEN_MODEL),
723
- provider=safe_get(kwargs, 'vgprovider', NPCSH_VIDEO_GEN_PROVIDER),
724
- npc=safe_get(kwargs, 'npc'),
725
- num_frames = num_frames,
726
- width = width,
727
- height = height,
728
- output_path=output_path,
729
-
730
- **safe_get(kwargs, 'api_kwargs', {})
731
- )
732
- return result
733
- except Exception as e:
734
- traceback.print_exc()
735
- return {"output": f"Error generating video: {e}", "messages": messages}
736
-
737
-
738
- @router.route("sample", "Send a prompt directly to the LLM")
739
- def sample_handler(command: str, **kwargs):
740
- messages = safe_get(kwargs, "messages", [])
741
-
742
-
743
- positional_args = safe_get(kwargs, 'positional_args', [])
744
- prompt = " ".join(positional_args)
745
121
 
746
- if not prompt:
747
- return {"output": "Usage: /sample <your prompt> [-m --model] model [-p --provider] provider",
748
- "messages": messages}
749
-
750
- try:
751
- result = get_llm_response(
752
- prompt=prompt,
753
- **kwargs
754
- )
755
- if result and isinstance(result, dict):
756
- return {
757
- "output": result.get('response'),
758
- "messages": result.get('messages', messages),
759
- "model": kwargs.get('model'),
760
- "provider":kwargs.get('provider'),
761
- "npc":kwargs.get("npc"),
762
- }
763
- else:
764
-
765
- return {"output": str(result), "messages": messages}
766
-
767
- except Exception as e:
768
- traceback.print_exc()
769
- return {"output": f"Error sampling LLM: {e}", "messages": messages}
770
-
771
-
772
-
773
- @router.route("search", "Execute web search or memory/KG search")
774
- def search_handler(command: str, **kwargs):
775
- messages = safe_get(kwargs, "messages", [])
776
-
777
- positional_args = safe_get(kwargs, 'positional_args', [])
778
-
779
- search_type = None
780
- query_parts = []
781
-
782
- i = 0
783
- while i < len(positional_args):
784
- arg = positional_args[i]
785
- if arg in ['-m', '-mem', '--memory']:
786
- search_type = 'memory'
787
- i += 1
788
- elif arg in ['-kg', '--knowledge-graph']:
789
- search_type = 'kg'
790
- i += 1
791
- else:
792
- query_parts.append(arg)
793
- i += 1
794
-
795
- query = " ".join(query_parts)
796
-
797
- if not query:
798
- return {
799
- "output": (
800
- "Usage:\n"
801
- " /search <query> - Web search\n"
802
- " /search -m <query> - Memory search\n"
803
- " /search -kg <query> - Knowledge graph search"
804
- ),
805
- "messages": messages
806
- }
807
-
808
- if search_type == 'memory':
809
- return search_memories(query, kwargs, messages)
810
- elif search_type == 'kg':
811
- return search_knowledge_graph(query, kwargs, messages)
812
- else:
813
- return search_web_default(query, kwargs, messages)
814
-
815
- def search_memories(query: str, kwargs: dict, messages: list):
816
- command_history = kwargs.get('command_history')
817
-
818
- if not command_history:
819
- db_path = safe_get(
820
- kwargs,
821
- "history_db_path",
822
- os.path.expanduser('~/npcsh_history.db')
823
- )
824
- try:
825
- command_history = CommandHistory(db_path)
826
- except Exception as e:
827
- return {
828
- "output": f"Error connecting to history: {e}",
829
- "messages": messages
830
- }
831
-
832
- state = kwargs.get('state')
833
- npc = safe_get(kwargs, 'npc')
834
- team = safe_get(kwargs, 'team')
835
-
836
- npc_name = npc.name if isinstance(npc, NPC) else "__none__"
837
- team_name = team.name if team else "__none__"
838
- current_path = safe_get(kwargs, 'current_path', os.getcwd())
839
-
840
- try:
841
- memories = get_relevant_memories(
842
- command_history=command_history,
843
- npc_name=npc_name,
844
- team_name=team_name,
845
- path=current_path,
846
- query=query,
847
- max_memories=10,
848
- state=state
849
- )
850
-
851
- if not memories:
852
- output = f"No memories found for query: '{query}'"
853
- else:
854
- output = f"Found {len(memories)} memories:\n\n"
855
- for i, mem in enumerate(memories, 1):
856
- final_mem = (
857
- mem.get('final_memory') or
858
- mem.get('initial_memory')
859
- )
860
- timestamp = mem.get('timestamp', 'unknown')
861
- output += f"{i}. [{timestamp}] {final_mem}\n"
862
-
863
- return {"output": output, "messages": messages}
864
-
865
- except Exception as e:
866
- import traceback
867
- traceback.print_exc()
868
- return {
869
- "output": f"Error searching memories: {e}",
870
- "messages": messages
871
- }
872
-
873
- def search_knowledge_graph(query: str, kwargs: dict, messages: list):
874
- command_history = kwargs.get('command_history')
875
-
876
- if not command_history:
877
- db_path = safe_get(
878
- kwargs,
879
- "history_db_path",
880
- os.path.expanduser('~/npcsh_history.db')
881
- )
882
- try:
883
- command_history = CommandHistory(db_path)
884
- except Exception as e:
885
- return {
886
- "output": f"Error connecting to history: {e}",
887
- "messages": messages
888
- }
889
-
890
- npc = safe_get(kwargs, 'npc')
891
- team = safe_get(kwargs, 'team')
892
-
893
- npc_name = npc.name if isinstance(npc, NPC) else "__none__"
894
- team_name = team.name if team else "__none__"
895
- current_path = safe_get(kwargs, 'current_path', os.getcwd())
896
-
897
- try:
898
- engine = command_history.engine
899
- kg = load_kg_from_db(
900
- engine,
901
- team_name,
902
- npc_name,
903
- current_path
904
- )
905
-
906
- if not kg or not kg.get('facts'):
907
- return {
908
- "output": (
909
- f"No knowledge graph found for current scope.\n"
910
- f"Scope: Team='{team_name}', "
911
- f"NPC='{npc_name}', Path='{current_path}'"
912
- ),
913
- "messages": messages
914
- }
915
-
916
- query_lower = query.lower()
917
- matching_facts = []
918
- matching_concepts = []
919
-
920
- for fact in kg.get('facts', []):
921
- statement = fact.get('statement', '').lower()
922
- if query_lower in statement:
923
- matching_facts.append(fact)
924
-
925
- for concept in kg.get('concepts', []):
926
- name = concept.get('name', '').lower()
927
- desc = concept.get('description', '').lower()
928
- if query_lower in name or query_lower in desc:
929
- matching_concepts.append(concept)
930
-
931
- output = f"Knowledge Graph Search Results for '{query}':\n\n"
932
-
933
- if matching_facts:
934
- output += f"## Facts ({len(matching_facts)}):\n"
935
- for i, fact in enumerate(matching_facts, 1):
936
- output += f"{i}. {fact.get('statement')}\n"
937
- output += "\n"
938
-
939
- if matching_concepts:
940
- output += f"## Concepts ({len(matching_concepts)}):\n"
941
- for i, concept in enumerate(matching_concepts, 1):
942
- name = concept.get('name')
943
- desc = concept.get('description', '')
944
- output += f"{i}. {name}: {desc}\n"
945
-
946
- if not matching_facts and not matching_concepts:
947
- output += "No matching facts or concepts found."
948
-
949
- return {"output": output, "messages": messages}
950
-
951
- except Exception as e:
952
- import traceback
953
- traceback.print_exc()
954
- return {
955
- "output": f"Error searching KG: {e}",
956
- "messages": messages
957
- }
958
-
959
- def search_web_default(query: str, kwargs: dict, messages: list):
960
- search_provider = safe_get(kwargs, 'sprovider', NPCSH_SEARCH_PROVIDER)
961
- render_markdown(f'- Searching {search_provider} for "{query}"')
962
-
963
- try:
964
- search_results = search_web(query, provider=search_provider)
965
- output = (
966
- "\n".join([f"- {res}" for res in search_results])
967
- if search_results
968
- else "No results found."
969
- )
970
- except Exception as e:
971
- import traceback
972
- traceback.print_exc()
973
- output = f"Error during web search: {e}"
974
-
975
- return {"output": output, "messages": messages}
976
-
977
-
978
-
979
- @router.route("serve", "Serve an NPC Team")
980
- def serve_handler(command: str, **kwargs):
981
-
982
-
983
-
984
- port = safe_get(kwargs, "port", 5337)
985
-
986
- messages = safe_get(kwargs, "messages", [])
987
- cors = safe_get(kwargs, "cors", None)
988
- if cors:
989
- cors_origins = [origin.strip() for origin in cors.split(",")]
990
- else:
991
- cors_origins = None
992
-
993
- start_flask_server(
994
- port=port,
995
- cors_origins=cors_origins,
996
- )
997
-
998
-
999
- return {"output": None, "messages": messages}
1000
-
1001
- @router.route("set", "Set configuration values")
1002
- def set_handler(command: str, **kwargs):
1003
- messages = safe_get(kwargs, "messages", [])
1004
- parts = command.split(maxsplit=1)
1005
- if len(parts) < 2 or '=' not in parts[1]:
1006
- return {"output": "Usage: /set <key>=<value>", "messages": messages}
1007
-
1008
- key_value = parts[1]
1009
- key, value = key_value.split('=', 1)
1010
- key = key.strip()
1011
- value = value.strip().strip('"\'')
1012
-
1013
- try:
1014
- set_npcsh_config_value(key, value)
1015
- output = f"Configuration value '{key}' set."
1016
- except NameError:
1017
- output = "Set function (set_npcsh_config_value) not available."
1018
- except Exception as e:
1019
- traceback.print_exc()
1020
- output = f"Error setting configuration '{key}': {e}"
1021
- return {"output": output, "messages": messages}
1022
-
1023
- @router.route("sleep", "Evolve knowledge graph. Use --dream to also run creative synthesis.")
1024
- def sleep_handler(command: str, **kwargs):
1025
- messages = safe_get(kwargs, "messages", [])
1026
- npc = safe_get(kwargs, 'npc')
1027
- team = safe_get(kwargs, 'team')
1028
- model = safe_get(kwargs, 'model')
1029
- provider = safe_get(kwargs, 'provider')
1030
-
1031
- is_dreaming = safe_get(kwargs, 'dream', False)
1032
- operations_str = safe_get(kwargs, 'ops')
1033
-
1034
- operations_config = None
1035
- if operations_str and isinstance(operations_str, str):
1036
- operations_config = [op.strip() for op in operations_str.split(',')]
1037
-
1038
-
1039
- team_name = team.name if team else "__none__"
1040
- npc_name = npc.name if isinstance(npc, NPC) else "__none__"
1041
- current_path = os.getcwd()
1042
- scope_str = f"Team: '{team_name}', NPC: '{npc_name}', Path: '{current_path}'"
1043
-
1044
-
1045
- render_markdown(f"- Checking knowledge graph for scope: {scope_str}")
1046
-
1047
- try:
1048
- db_path = os.getenv("NPCSH_DB_PATH", os.path.expanduser("~/npcsh_history.db"))
1049
- command_history = CommandHistory(db_path)
1050
- engine = command_history.engine
1051
- except Exception as e:
1052
- return {"output": f"Error connecting to history database for KG access: {e}", "messages": messages}
1053
-
1054
- try:
1055
- current_kg = load_kg_from_db(engine, team_name, npc_name, current_path)
1056
-
1057
-
1058
- if not current_kg or not current_kg.get('facts'):
1059
- output_msg = f"Knowledge graph for the current scope is empty. Nothing to process.\n"
1060
- output_msg += f" - Scope Checked: {scope_str}\n\n"
1061
- output_msg += "**Hint:** Have a conversation or run some commands first to build up knowledge in this specific context. The KG is unique to each combination of Team, NPC, and directory."
1062
- return {"output": output_msg, "messages": messages}
1063
-
1064
-
1065
- original_facts = len(current_kg.get('facts', []))
1066
- original_concepts = len(current_kg.get('concepts', []))
1067
-
1068
-
1069
-
1070
-
1071
- process_type = "Sleep"
1072
- ops_display = f"with operations: {operations_config}" if operations_config else "with random operations"
1073
- render_markdown(f"- Initiating sleep process {ops_display}")
1074
-
1075
- evolved_kg, _ = kg_sleep_process(
1076
- existing_kg=current_kg,
1077
- model=model,
1078
- provider=provider,
1079
- npc=npc,
1080
- operations_config=operations_config
1081
- )
1082
-
1083
-
1084
- if is_dreaming:
1085
- process_type += " & Dream"
1086
- render_markdown(f"- Initiating dream process on the evolved KG...")
1087
- evolved_kg, _ = kg_dream_process(
1088
- existing_kg=evolved_kg,
1089
- model=model,
1090
- provider=provider,
1091
- npc=npc
1092
- )
1093
-
1094
-
1095
- save_kg_to_db(conn, evolved_kg, team_name, npc_name, current_path)
1096
-
1097
-
1098
- new_facts = len(evolved_kg.get('facts', []))
1099
- new_concepts = len(evolved_kg.get('concepts', []))
1100
-
1101
- output = f"{process_type} process complete.\n"
1102
- output += f"- Facts: {original_facts} -> {new_facts} ({new_facts - original_facts:+})\n"
1103
- output += f"- Concepts: {original_concepts} -> {new_concepts} ({new_concepts - original_concepts:+})"
1104
-
1105
- print(evolved_kg.get('facts'))
1106
- print(evolved_kg.get('concepts'))
1107
-
1108
- return {"output": output, "messages": messages}
1109
-
1110
- except Exception as e:
1111
- import traceback
1112
- traceback.print_exc()
1113
- return {"output": f"Error during KG evolution process: {e}", "messages": messages}
1114
- finally:
1115
- if 'command_history' in locals() and command_history:
1116
- command_history.close()
1117
-
1118
-
1119
-
1120
-
1121
- @router.route("spool", "Enter interactive chat (spool) mode")
1122
- def spool_handler(command: str, **kwargs):
1123
- try:
1124
- npc = safe_get(kwargs, 'npc')
1125
- team = safe_get(kwargs, 'team')
1126
-
1127
- if isinstance(npc, str) and team:
1128
- npc_name = npc
1129
- if npc_name in team.npcs:
1130
- npc = team.npcs[npc_name]
1131
- else:
1132
- return {"output": f"Error: NPC '{npc_name}' not found in team. Available NPCs: {', '.join(team.npcs.keys())}", "messages": safe_get(kwargs, "messages", [])}
1133
- kwargs['npc'] = npc
1134
- return enter_spool_mode(
1135
- **kwargs)
1136
- except Exception as e:
1137
- traceback.print_exc()
1138
- return {"output": f"Error entering spool mode: {e}", "messages": safe_get(kwargs, "messages", [])}
1139
-
1140
- @router.route("jinxs", "Show available jinxs for the current NPC/Team")
1141
- def jinxs_handler(command: str, **kwargs):
1142
- npc = safe_get(kwargs, 'npc')
1143
- team = safe_get(kwargs, 'team')
1144
- output = "Available Jinxs:\n"
1145
- jinxs_listed = set()
1146
-
1147
- def format_jinx(name, jinx_obj):
1148
- desc = getattr(jinx_obj, 'description', 'No description available.')
1149
- return f"- /{name}: {desc}\n"
1150
-
1151
- if npc and isinstance(npc, NPC) and hasattr(npc, 'jinxs_dict') and npc.jinxs_dict:
1152
- output += f"\n--- Jinxs for NPC: {npc.name} ---\n"
1153
- for name, jinx in sorted(npc.jinxs_dict.items()):
1154
- output += format_jinx(name, jinx)
1155
- jinxs_listed.add(name)
1156
-
1157
- if team and hasattr(team, 'jinxs_dict') and team.jinxs_dict:
1158
- team_has_jinxs = False
1159
- team_output = ""
1160
- for name, jinx in sorted(team.jinxs_dict.items()):
1161
- if name not in jinxs_listed:
1162
- team_output += format_jinx(name, jinx)
1163
- team_has_jinxs = True
1164
- if team_has_jinxs:
1165
- output += f"\n--- Jinxs for Team: {getattr(team, 'name', 'Unnamed Team')} ---\n"
1166
- output += team_output
1167
-
1168
- if not jinxs_listed and not (team and hasattr(team, 'jinxs_dict') and team.jinxs_dict):
1169
- output = "No jinxs available for the current context."
1170
-
1171
- return {"output": output.strip(), "messages": safe_get(kwargs, "messages", [])}
1172
-
1173
- @router.route("trigger", "Execute a trigger command")
1174
- def trigger_handler(command: str, **kwargs):
1175
- messages = safe_get(kwargs, "messages", [])
1176
- user_command = " ".join(command.split()[1:])
1177
- if not user_command:
1178
- return {"output": "Usage: /trigger <trigger_description>", "messages": messages}
1179
- try:
1180
- return execute_trigger_command(command=user_command, **kwargs)
1181
- except NameError:
1182
- return {"output": "Trigger function (execute_trigger_command) not available.", "messages": messages}
1183
- except Exception as e:
1184
- traceback.print_exc()
1185
- return {"output": f"Error executing trigger: {e}", "messages": messages}
1186
- @router.route("vixynt", "Generate images from text descriptions")
1187
- def vixynt_handler(command: str, **kwargs):
1188
- npc = safe_get(kwargs, 'npc')
1189
- model = safe_get(kwargs, 'igmodel', NPCSH_IMAGE_GEN_MODEL)
1190
- provider = safe_get(kwargs, 'igprovider', NPCSH_IMAGE_GEN_PROVIDER)
1191
- height = safe_get(kwargs, 'height', 1024)
1192
- width = safe_get(kwargs, 'width', 1024)
1193
- output_file_base = safe_get(kwargs, 'output_file')
1194
- attachments = safe_get(kwargs, 'attachments')
1195
- n_images = safe_get(kwargs, 'n_images', 1)
1196
- if isinstance(attachments, str):
1197
- attachments = attachments.split(',')
1198
-
1199
- messages = safe_get(kwargs, 'messages', [])
1200
-
1201
- user_prompt = " ".join(safe_get(kwargs, 'positional_args', []))
1202
-
1203
- if not user_prompt:
1204
- return {"output": "Usage: /vixynt <prompt> [--output_file path] [--attachments path] [--n_images num]", "messages": messages}
1205
-
1206
- try:
1207
-
1208
- images_list = gen_image(
1209
- prompt=user_prompt,
1210
- model=model,
1211
- provider=provider,
1212
- npc=npc,
1213
- height=height,
1214
- width=width,
1215
- n_images=n_images,
1216
- input_images=attachments
1217
- )
1218
-
1219
- saved_files = []
1220
- if not isinstance(images_list, list):
1221
- images_list = [images_list] if images_list is not None else []
1222
-
1223
- for i, image in enumerate(images_list):
1224
- if image is None:
1225
- continue
1226
-
1227
- if output_file_base is None:
1228
- os.makedirs(os.path.expanduser("~/.npcsh/images/"), exist_ok=True)
1229
- current_output_file = (
1230
- os.path.expanduser("~/.npcsh/images/")
1231
- + f"image_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{i}.png"
1232
- )
1233
- else:
1234
- base_name, ext = os.path.splitext(os.path.expanduser(output_file_base))
1235
- current_output_file = f"{base_name}_{i}{ext}"
1236
-
1237
- image.save(current_output_file)
1238
- image.show()
1239
- saved_files.append(current_output_file)
1240
-
1241
- if saved_files:
1242
- if attachments:
1243
- output = f"Image(s) edited and saved to: {', '.join(saved_files)}"
1244
- else:
1245
- output = f"Image(s) generated and saved to: {', '.join(saved_files)}"
1246
- else:
1247
- output = f"No images {'edited' if attachments else 'generated'}."
1248
-
1249
- except Exception as e:
1250
- traceback.print_exc()
1251
- output = f"Error {'editing' if attachments else 'generating'} image: {e}"
1252
-
1253
- return {
1254
- "output": output,
1255
- "messages": messages,
1256
- "model": model,
1257
- "provider": provider
1258
- }
1259
-
1260
-
1261
- @router.route("wander", "Enter wander mode (experimental)")
1262
- def wander_handler(command: str, **kwargs):
1263
- messages = safe_get(kwargs, "messages", [])
1264
-
1265
-
1266
- try:
1267
- parts = shlex.split(command)
1268
- problem_parts = []
1269
- wander_params = {}
1270
-
1271
- i = 1
1272
- while i < len(parts):
1273
- part = parts[i]
1274
-
1275
- if '=' in part:
1276
-
1277
- key, initial_value = part.split('=', 1)
1278
-
1279
-
1280
- value_parts = [initial_value]
1281
- j = i + 1
1282
- while j < len(parts) and '=' not in parts[j]:
1283
- value_parts.append(parts[j])
1284
- j += 1
1285
-
1286
-
1287
- wander_params[key] = " ".join(value_parts)
1288
-
1289
- i = j
1290
- else:
1291
-
1292
- problem_parts.append(part)
1293
- i += 1
1294
-
1295
- problem = " ".join(problem_parts)
1296
- except Exception as e:
1297
- return {"output": f"Error parsing arguments: {e}", "messages": messages}
1298
-
1299
- if not problem:
1300
- return {"output": "Usage: /wander <problem> [key=value...]", "messages": messages}
1301
-
1302
- try:
1303
-
1304
- mode_args = {
1305
- 'problem': problem,
1306
- 'npc': safe_get(kwargs, 'npc'),
1307
- 'model': safe_get(kwargs, 'model'),
1308
- 'provider': safe_get(kwargs, 'provider'),
1309
-
1310
- 'environment': wander_params.get('environment'),
1311
- 'low_temp': float(wander_params.get('low-temp', 0.5)),
1312
- 'high_temp': float(wander_params.get('high-temp', 1.9)),
1313
- 'interruption_likelihood': float(wander_params.get('interruption-likelihood', 1)),
1314
- 'sample_rate': float(wander_params.get('sample-rate', 0.4)),
1315
- 'n_high_temp_streams': int(wander_params.get('n-high-temp-streams', 5)),
1316
- 'include_events': bool(wander_params.get('include-events', False)),
1317
- 'num_events': int(wander_params.get('num-events', 3))
1318
- }
1319
-
1320
- result = enter_wander_mode(**mode_args)
1321
-
1322
- if isinstance(result, list) and result:
1323
- output = result[-1].get("insight", "Wander mode session complete.")
1324
- else:
1325
- output = str(result) if result else "Wander mode session complete."
1326
-
1327
- messages.append({"role": "assistant", "content": output})
1328
- return {"output": output, "messages": messages}
1329
-
1330
- except Exception as e:
1331
- traceback.print_exc()
1332
- return {"output": f"Error during wander mode: {e}", "messages": messages}
1333
-
1334
- @router.route("yap", "Enter voice chat (yap) mode")
1335
- def yap_handler(command: str, **kwargs):
1336
- try:
1337
- return enter_yap_mode(
1338
- ** kwargs
1339
- )
1340
- except Exception as e:
1341
- traceback.print_exc()
1342
- return {"output": f"Error entering yap mode: {e}", "messages": safe_get(kwargs, "messages", [])}
1343
-
1344
- @router.route("alicanto", "Conduct deep research with multiple perspectives, identifying gold insights and cliff warnings")
1345
- def alicanto_handler(command: str, **kwargs):
1346
- messages = safe_get(kwargs, "messages", [])
1347
- parts = shlex.split(command)
1348
- skip_research = safe_get(kwargs, "skip_research", True)
1349
- query = ""
1350
- num_npcs = safe_get(kwargs, 'num_npcs', 5)
1351
- depth = safe_get(kwargs, 'depth', 3)
1352
-
1353
- i = 1
1354
- while i < len(parts):
1355
- if parts[i].startswith('--'):
1356
- option = parts[i][2:]
1357
- if option in ['num-npcs', 'npcs']:
1358
- if i + 1 < len(parts) and parts[i + 1].isdigit():
1359
- num_npcs = int(parts[i + 1])
1360
- i += 2
1361
- else:
1362
- i += 1
1363
- elif option in ['depth', 'd']:
1364
- if i + 1 < len(parts) and parts[i + 1].isdigit():
1365
- depth = int(parts[i + 1])
1366
- i += 2
1367
- else:
1368
- i += 1
1369
- elif option in ['exploration', 'e']:
1370
- if i + 1 < len(parts) and parts[i + 1].replace('.', '', 1).isdigit():
1371
- exploration_factor = float(parts[i + 1])
1372
- i += 2
1373
- else:
1374
- i += 1
1375
- elif option in ['creativity', 'c']:
1376
- if i + 1 < len(parts) and parts[i + 1].replace('.', '', 1).isdigit():
1377
- creativity_factor = float(parts[i + 1])
1378
- i += 2
1379
- else:
1380
- i += 1
1381
- elif option in ['format', 'f']:
1382
- if i + 1 < len(parts):
1383
- output_format = parts[i + 1]
1384
- i += 2
1385
- else:
1386
- i += 1
1387
- else:
1388
-
1389
- i += 1
1390
- else:
1391
-
1392
- query += parts[i] + " "
1393
- i += 1
1394
-
1395
- query = query.strip()
1396
-
1397
-
1398
- if 'num_npcs' in kwargs:
1399
- try:
1400
- num_npcs = int(kwargs['num_npcs'])
1401
- except ValueError:
1402
- return {"output": "Error: num_npcs must be an integer", "messages": messages}
1403
-
1404
- if 'depth' in kwargs:
1405
- try:
1406
- depth = int(kwargs['depth'])
1407
- except ValueError:
1408
- return {"output": "Error: depth must be an integer", "messages": messages}
1409
-
1410
- if 'exploration' in kwargs:
1411
- try:
1412
- exploration_factor = float(kwargs['exploration'])
1413
- except ValueError:
1414
- return {"output": "Error: exploration must be a float", "messages": messages}
1415
-
1416
- if 'creativity' in kwargs:
1417
- try:
1418
- creativity_factor = float(kwargs['creativity'])
1419
- except ValueError:
1420
- return {"output": "Error: creativity must be a float", "messages": messages}
1421
-
1422
- if not query:
1423
- return {"output": "Usage: /alicanto <research query> [--num-npcs N] [--depth N] [--exploration 0.3] [--creativity 0.5] [--format report|summary|full]", "messages": messages}
1424
-
1425
- try:
1426
- logging.info(f"Starting Alicanto research on: {query}")
1427
- model = safe_get(kwargs, 'model')
1428
- if len(model) == 0 :
1429
- model = NPCSH_CHAT_MODEL
1430
- provider = safe_get(kwargs, 'provider')
1431
- if len(provider) == 0 :
1432
- provider = NPCSH_CHAT_PROVIDER
1433
-
1434
-
1435
- print('model: ', model)
1436
- print('provider: ', provider)
1437
-
1438
- result = alicanto(
1439
- query,
1440
- num_npcs=num_npcs,
1441
- depth=depth,
1442
- model=model,
1443
- provider=provider,
1444
- max_steps = safe_get(kwargs, 'max_steps', 20),
1445
- skip_research = skip_research
1446
-
1447
- )
1448
-
1449
-
1450
- if isinstance(result, dict):
1451
- if "integration" in result:
1452
- output = result["integration"]
1453
- else:
1454
- output = "Alicanto research completed. Full results available in returned data."
1455
- else:
1456
- output = result
1457
-
1458
- return {"output": output, "messages": messages, "alicanto_result": result}
1459
- except Exception as e:
1460
- traceback.print_exc()
1461
- logging.error(f"Error during Alicanto research: {e}")
1462
- return {"output": f"Error during Alicanto research: {e}", "messages": messages}
122
+ router = CommandRouter()