npcpy 1.0.26__py3-none-any.whl → 1.2.32__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 (148) hide show
  1. npcpy/__init__.py +0 -7
  2. npcpy/data/audio.py +16 -99
  3. npcpy/data/image.py +43 -42
  4. npcpy/data/load.py +83 -124
  5. npcpy/data/text.py +28 -28
  6. npcpy/data/video.py +8 -32
  7. npcpy/data/web.py +51 -23
  8. npcpy/ft/diff.py +110 -0
  9. npcpy/ft/ge.py +115 -0
  10. npcpy/ft/memory_trainer.py +171 -0
  11. npcpy/ft/model_ensembler.py +357 -0
  12. npcpy/ft/rl.py +360 -0
  13. npcpy/ft/sft.py +248 -0
  14. npcpy/ft/usft.py +128 -0
  15. npcpy/gen/audio_gen.py +24 -0
  16. npcpy/gen/embeddings.py +13 -13
  17. npcpy/gen/image_gen.py +262 -117
  18. npcpy/gen/response.py +615 -415
  19. npcpy/gen/video_gen.py +53 -7
  20. npcpy/llm_funcs.py +1869 -437
  21. npcpy/main.py +1 -1
  22. npcpy/memory/command_history.py +844 -510
  23. npcpy/memory/kg_vis.py +833 -0
  24. npcpy/memory/knowledge_graph.py +892 -1845
  25. npcpy/memory/memory_processor.py +81 -0
  26. npcpy/memory/search.py +188 -90
  27. npcpy/mix/debate.py +192 -3
  28. npcpy/npc_compiler.py +1672 -801
  29. npcpy/npc_sysenv.py +593 -1266
  30. npcpy/serve.py +3120 -0
  31. npcpy/sql/ai_function_tools.py +257 -0
  32. npcpy/sql/database_ai_adapters.py +186 -0
  33. npcpy/sql/database_ai_functions.py +163 -0
  34. npcpy/sql/model_runner.py +19 -19
  35. npcpy/sql/npcsql.py +706 -507
  36. npcpy/sql/sql_model_compiler.py +156 -0
  37. npcpy/tools.py +183 -0
  38. npcpy/work/plan.py +13 -279
  39. npcpy/work/trigger.py +3 -3
  40. npcpy-1.2.32.dist-info/METADATA +803 -0
  41. npcpy-1.2.32.dist-info/RECORD +54 -0
  42. npcpy/data/dataframes.py +0 -171
  43. npcpy/memory/deep_research.py +0 -125
  44. npcpy/memory/sleep.py +0 -557
  45. npcpy/modes/_state.py +0 -78
  46. npcpy/modes/alicanto.py +0 -1075
  47. npcpy/modes/guac.py +0 -785
  48. npcpy/modes/mcp_npcsh.py +0 -822
  49. npcpy/modes/npc.py +0 -213
  50. npcpy/modes/npcsh.py +0 -1158
  51. npcpy/modes/plonk.py +0 -409
  52. npcpy/modes/pti.py +0 -234
  53. npcpy/modes/serve.py +0 -1637
  54. npcpy/modes/spool.py +0 -312
  55. npcpy/modes/wander.py +0 -549
  56. npcpy/modes/yap.py +0 -572
  57. npcpy/npc_team/alicanto.npc +0 -2
  58. npcpy/npc_team/alicanto.png +0 -0
  59. npcpy/npc_team/assembly_lines/test_pipeline.py +0 -181
  60. npcpy/npc_team/corca.npc +0 -13
  61. npcpy/npc_team/foreman.npc +0 -7
  62. npcpy/npc_team/frederic.npc +0 -6
  63. npcpy/npc_team/frederic4.png +0 -0
  64. npcpy/npc_team/guac.png +0 -0
  65. npcpy/npc_team/jinxs/automator.jinx +0 -18
  66. npcpy/npc_team/jinxs/bash_executer.jinx +0 -31
  67. npcpy/npc_team/jinxs/calculator.jinx +0 -11
  68. npcpy/npc_team/jinxs/edit_file.jinx +0 -96
  69. npcpy/npc_team/jinxs/file_chat.jinx +0 -14
  70. npcpy/npc_team/jinxs/gui_controller.jinx +0 -28
  71. npcpy/npc_team/jinxs/image_generation.jinx +0 -29
  72. npcpy/npc_team/jinxs/internet_search.jinx +0 -30
  73. npcpy/npc_team/jinxs/local_search.jinx +0 -152
  74. npcpy/npc_team/jinxs/npcsh_executor.jinx +0 -31
  75. npcpy/npc_team/jinxs/python_executor.jinx +0 -8
  76. npcpy/npc_team/jinxs/screen_cap.jinx +0 -25
  77. npcpy/npc_team/jinxs/sql_executor.jinx +0 -33
  78. npcpy/npc_team/kadiefa.npc +0 -3
  79. npcpy/npc_team/kadiefa.png +0 -0
  80. npcpy/npc_team/npcsh.ctx +0 -9
  81. npcpy/npc_team/npcsh_sibiji.png +0 -0
  82. npcpy/npc_team/plonk.npc +0 -2
  83. npcpy/npc_team/plonk.png +0 -0
  84. npcpy/npc_team/plonkjr.npc +0 -2
  85. npcpy/npc_team/plonkjr.png +0 -0
  86. npcpy/npc_team/sibiji.npc +0 -5
  87. npcpy/npc_team/sibiji.png +0 -0
  88. npcpy/npc_team/spool.png +0 -0
  89. npcpy/npc_team/templates/analytics/celona.npc +0 -0
  90. npcpy/npc_team/templates/hr_support/raone.npc +0 -0
  91. npcpy/npc_team/templates/humanities/eriane.npc +0 -4
  92. npcpy/npc_team/templates/it_support/lineru.npc +0 -0
  93. npcpy/npc_team/templates/marketing/slean.npc +0 -4
  94. npcpy/npc_team/templates/philosophy/maurawa.npc +0 -0
  95. npcpy/npc_team/templates/sales/turnic.npc +0 -4
  96. npcpy/npc_team/templates/software/welxor.npc +0 -0
  97. npcpy/npc_team/yap.png +0 -0
  98. npcpy/routes.py +0 -958
  99. npcpy/work/mcp_helpers.py +0 -357
  100. npcpy/work/mcp_server.py +0 -194
  101. npcpy-1.0.26.data/data/npcpy/npc_team/alicanto.npc +0 -2
  102. npcpy-1.0.26.data/data/npcpy/npc_team/alicanto.png +0 -0
  103. npcpy-1.0.26.data/data/npcpy/npc_team/automator.jinx +0 -18
  104. npcpy-1.0.26.data/data/npcpy/npc_team/bash_executer.jinx +0 -31
  105. npcpy-1.0.26.data/data/npcpy/npc_team/calculator.jinx +0 -11
  106. npcpy-1.0.26.data/data/npcpy/npc_team/celona.npc +0 -0
  107. npcpy-1.0.26.data/data/npcpy/npc_team/corca.npc +0 -13
  108. npcpy-1.0.26.data/data/npcpy/npc_team/edit_file.jinx +0 -96
  109. npcpy-1.0.26.data/data/npcpy/npc_team/eriane.npc +0 -4
  110. npcpy-1.0.26.data/data/npcpy/npc_team/file_chat.jinx +0 -14
  111. npcpy-1.0.26.data/data/npcpy/npc_team/foreman.npc +0 -7
  112. npcpy-1.0.26.data/data/npcpy/npc_team/frederic.npc +0 -6
  113. npcpy-1.0.26.data/data/npcpy/npc_team/frederic4.png +0 -0
  114. npcpy-1.0.26.data/data/npcpy/npc_team/guac.png +0 -0
  115. npcpy-1.0.26.data/data/npcpy/npc_team/gui_controller.jinx +0 -28
  116. npcpy-1.0.26.data/data/npcpy/npc_team/image_generation.jinx +0 -29
  117. npcpy-1.0.26.data/data/npcpy/npc_team/internet_search.jinx +0 -30
  118. npcpy-1.0.26.data/data/npcpy/npc_team/kadiefa.npc +0 -3
  119. npcpy-1.0.26.data/data/npcpy/npc_team/kadiefa.png +0 -0
  120. npcpy-1.0.26.data/data/npcpy/npc_team/lineru.npc +0 -0
  121. npcpy-1.0.26.data/data/npcpy/npc_team/local_search.jinx +0 -152
  122. npcpy-1.0.26.data/data/npcpy/npc_team/maurawa.npc +0 -0
  123. npcpy-1.0.26.data/data/npcpy/npc_team/npcsh.ctx +0 -9
  124. npcpy-1.0.26.data/data/npcpy/npc_team/npcsh_executor.jinx +0 -31
  125. npcpy-1.0.26.data/data/npcpy/npc_team/npcsh_sibiji.png +0 -0
  126. npcpy-1.0.26.data/data/npcpy/npc_team/plonk.npc +0 -2
  127. npcpy-1.0.26.data/data/npcpy/npc_team/plonk.png +0 -0
  128. npcpy-1.0.26.data/data/npcpy/npc_team/plonkjr.npc +0 -2
  129. npcpy-1.0.26.data/data/npcpy/npc_team/plonkjr.png +0 -0
  130. npcpy-1.0.26.data/data/npcpy/npc_team/python_executor.jinx +0 -8
  131. npcpy-1.0.26.data/data/npcpy/npc_team/raone.npc +0 -0
  132. npcpy-1.0.26.data/data/npcpy/npc_team/screen_cap.jinx +0 -25
  133. npcpy-1.0.26.data/data/npcpy/npc_team/sibiji.npc +0 -5
  134. npcpy-1.0.26.data/data/npcpy/npc_team/sibiji.png +0 -0
  135. npcpy-1.0.26.data/data/npcpy/npc_team/slean.npc +0 -4
  136. npcpy-1.0.26.data/data/npcpy/npc_team/spool.png +0 -0
  137. npcpy-1.0.26.data/data/npcpy/npc_team/sql_executor.jinx +0 -33
  138. npcpy-1.0.26.data/data/npcpy/npc_team/test_pipeline.py +0 -181
  139. npcpy-1.0.26.data/data/npcpy/npc_team/turnic.npc +0 -4
  140. npcpy-1.0.26.data/data/npcpy/npc_team/welxor.npc +0 -0
  141. npcpy-1.0.26.data/data/npcpy/npc_team/yap.png +0 -0
  142. npcpy-1.0.26.dist-info/METADATA +0 -827
  143. npcpy-1.0.26.dist-info/RECORD +0 -139
  144. npcpy-1.0.26.dist-info/entry_points.txt +0 -11
  145. /npcpy/{modes → ft}/__init__.py +0 -0
  146. {npcpy-1.0.26.dist-info → npcpy-1.2.32.dist-info}/WHEEL +0 -0
  147. {npcpy-1.0.26.dist-info → npcpy-1.2.32.dist-info}/licenses/LICENSE +0 -0
  148. {npcpy-1.0.26.dist-info → npcpy-1.2.32.dist-info}/top_level.txt +0 -0
npcpy/npc_sysenv.py CHANGED
@@ -1,54 +1,21 @@
1
- import re
2
1
  from datetime import datetime
3
-
4
- import os
5
- import sqlite3
6
2
  from dotenv import load_dotenv
7
3
  import logging
8
- from typing import List, Any
9
-
10
- import subprocess
11
- import platform
12
-
13
- try:
14
- import nltk
15
- except:
16
- print("Error importing nltk")
17
- import numpy as np
18
-
19
- import filecmp
20
-
21
- import shutil
22
- import tempfile
23
- import pandas as pd
24
-
25
- try:
26
- from sentence_transformers import util
27
- except Exception as e:
28
- print(f"Error importing sentence_transformers: {e}")
29
-
30
-
31
-
32
-
33
- from typing import Dict, Any, List, Optional, Union
34
- import numpy as np
35
- from colorama import Fore, Back, Style
36
4
  import re
37
- import tempfile
5
+ import os
6
+ import socket
7
+ import concurrent.futures
8
+ import platform
38
9
  import sqlite3
39
- from datetime import datetime
40
- import logging
41
- import textwrap
42
- import subprocess
43
- from termcolor import colored
44
- import sys
45
- # --- Platform-specific imports and guards ---
46
10
  import sys
47
- import platform
11
+ from typing import Dict, List
12
+ import textwrap
13
+ import json
14
+
48
15
 
16
+ import requests
49
17
  ON_WINDOWS = platform.system() == "Windows"
50
18
 
51
- # Try/except for termios, tty, pty, select, signal
52
19
  try:
53
20
  if not ON_WINDOWS:
54
21
  import termios
@@ -63,14 +30,14 @@ except ImportError:
63
30
  select = None
64
31
  signal = None
65
32
 
66
- # Try/except for readline
33
+
67
34
  try:
68
35
  import readline
69
36
  except ImportError:
70
37
  readline = None
71
- print('no readline support, some features may not work as desired.')
38
+ logging.warning('no readline support, some features may not work as desired.')
39
+
72
40
 
73
- # Try/except for rich imports
74
41
  try:
75
42
  from rich.console import Console
76
43
  from rich.markdown import Markdown
@@ -83,7 +50,7 @@ except ImportError:
83
50
  import warnings
84
51
  import time
85
52
 
86
- # Global variables
53
+
87
54
  running = True
88
55
  is_recording = False
89
56
  recording_data = []
@@ -96,9 +63,18 @@ warnings.filterwarnings("ignore", module="torch.serialization")
96
63
  os.environ["PYTHONWARNINGS"] = "ignore"
97
64
  os.environ["SDL_AUDIODRIVER"] = "dummy"
98
65
 
66
+ def check_internet_connection(timeout=5):
67
+ """
68
+ Checks for internet connectivity by trying to connect to a well-known host.
69
+ """
70
+ try:
71
+ socket.create_connection(("8.8.8.8", 53), timeout=timeout)
72
+ return True
73
+ except OSError:
74
+ return False
99
75
 
100
76
 
101
- def get_locally_available_models(project_directory):
77
+ def get_locally_available_models(project_directory, airplane_mode=False):
102
78
  available_models = {}
103
79
  env_path = os.path.join(project_directory, ".env")
104
80
  env_vars = {}
@@ -111,176 +87,228 @@ def get_locally_available_models(project_directory):
111
87
  key, value = line.split("=", 1)
112
88
  env_vars[key.strip()] = value.strip().strip("\"'")
113
89
 
114
- if "ANTHROPIC_API_KEY" in env_vars or os.environ.get("ANTHROPIC_API_KEY"):
115
- try:
116
- import anthropic
117
-
118
- client = anthropic.Anthropic(api_key=env_vars.get("ANTHROPIC_API_KEY") or os.environ.get("ANTHROPIC_API_KEY"))
119
- models = client.models.list()
120
- for model in models.data:
121
- available_models[model.id] = 'anthropic'
90
+ internet_available = check_internet_connection()
91
+ if not internet_available:
92
+ logging.info(
93
+ "No internet connection detected. "
94
+ "External API calls will be skipped."
95
+ )
96
+ airplane_mode = True
97
+ else:
98
+ logging.info(
99
+ "Internet connection detected. "
100
+ "Proceeding based on 'airplane_mode' parameter."
101
+ )
102
+
103
+ custom_providers = load_custom_providers()
104
+
105
+ for provider_name, config in custom_providers.items():
106
+ api_key_var = config.get('api_key_var')
107
+ if not api_key_var:
108
+ api_key_var = f"{provider_name.upper()}_API_KEY"
109
+
110
+ if api_key_var in env_vars or os.environ.get(api_key_var):
111
+ try:
112
+ import requests
113
+
114
+ def fetch_custom_models():
115
+ base_url = config.get('base_url', '')
116
+ headers = config.get('headers', {})
122
117
 
123
- except:
124
- print("anthropic models not indexed")
125
- if "OPENAI_API_KEY" in env_vars or os.environ.get("OPENAI_API_KEY"):
126
- try:
127
- import openai
128
-
129
- openai.api_key = env_vars.get("OPENAI_API_KEY", None) or os.environ.get("OPENAI_API_KEY", None)
130
- models = openai.models.list()
131
-
132
- for model in models.data:
133
- if (
134
- (
135
- "gpt" in model.id
136
- or "o1" in model.id
137
- or "o3" in model.id
138
- or "chat" in model.id
118
+ api_key = env_vars.get(api_key_var) or \
119
+ os.environ.get(api_key_var)
120
+ if api_key:
121
+ headers['Authorization'] = f'Bearer {api_key}'
122
+
123
+ models_endpoint = f"{base_url.rstrip('/')}/models"
124
+ response = requests.get(
125
+ models_endpoint,
126
+ headers=headers,
127
+ timeout=3.5
139
128
  )
140
- and "audio" not in model.id
141
- and "realtime" not in model.id
142
- ):
143
- available_models[model.id] = "openai"
144
- except:
145
- print("openai models not indexed")
146
-
147
- if "GEMINI_API_KEY" in env_vars or os.environ.get("GEMINI_API_KEY"):
148
- try:
149
-
150
- from google import genai
129
+
130
+ if response.status_code == 200:
131
+ data = response.json()
132
+
133
+ if isinstance(data, dict) and 'data' in data:
134
+ return [
135
+ m['id'] for m in data['data']
136
+ if 'id' in m
137
+ ]
138
+ elif isinstance(data, list):
139
+ return [
140
+ m['id'] for m in data
141
+ if isinstance(m, dict) and 'id' in m
142
+ ]
143
+ return []
144
+
145
+ models = fetch_custom_models()
146
+ for model in models:
147
+ available_models[model] = 'openai-like'
148
+
149
+ logging.info(
150
+ f"Loaded {len(models)} models "
151
+ f"from custom provider '{provider_name}'"
152
+ )
153
+
154
+ except Exception as e:
155
+ logging.warning(
156
+ f"Failed to load models from "
157
+ f"custom provider '{provider_name}': {e}"
158
+ )
159
+
160
+
161
+ airplane_mode = False
162
+ if not airplane_mode:
163
+ timeout_seconds = 3.5
151
164
 
152
- #print('GEMINI_API_KEY', genai)
153
165
 
154
- client = genai.Client(api_key = env_vars.get("GEMINI_API_KEY") or os.environ.get("GEMINI_API_KEY"))
155
- models= []
156
- #import pdb
157
- #pdb.set_trace()
166
+ with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
167
+
168
+ if 'NPCSH_API_URL' in env_vars or os.environ.get('NPCSH_API_URL'):
169
+ try:
170
+ import requests
171
+
172
+ def fetch_custom_models():
173
+ base_url = env_vars.get('NPCSH_API_URL') or os.environ.get('NPCSH_API_URL')
174
+ models_endpoint = f"{base_url.rstrip('/')}/models"
175
+ response = requests.get(
176
+ models_endpoint,
177
+
178
+ timeout=3.5
179
+ )
180
+
181
+ if response.status_code == 200:
182
+ data = response.json()
183
+
184
+ if isinstance(data, dict) and 'data' in data:
185
+ return [
186
+ m['id'] for m in data['data']
187
+ if 'id' in m
188
+ ]
189
+ elif isinstance(data, list):
190
+ return [
191
+ m['id'] for m in data
192
+ if isinstance(m, dict) and 'id' in m
193
+ ]
194
+ return []
195
+
196
+ models = fetch_custom_models()
197
+ for model in models:
198
+ available_models[model] = 'openai-like'
199
+
200
+
201
+
202
+
203
+ except Exception as e:
204
+ logging.warning(
205
+ f"Failed to load models from "
206
+ f"custom provider 'openai-like': {e}"
207
+ )
208
+
158
209
 
159
- for m in client.models.list():
160
- for action in m.supported_actions:
161
- if action == "generateContent":
162
- if 'models/' in m.name:
163
- if m.name.split('/')[1] in ['gemini-2.5-pro', 'gemini-2.5-flash', 'gemini-2.0-flash', 'gemini-2.0-pro', 'gemini-1.5-pro', 'gemini-1.5-flash']:
164
- models.append(m.name.split('/')[1])
165
- for model in set(models):
166
- if "gemini" in model:
167
- available_models[model] = "gemini"
168
- except Exception as e:
169
- print(f"gemini models not indexed: {e}")
170
- if "DEEPSEEK_API_KEY" in env_vars or os.environ.get("DEEPSEEK_API_KEY"):
171
- available_models['deepseek-chat'] = 'deepseek'
172
- available_models['deepseek-reasoner'] = 'deepseek'
210
+ if "ANTHROPIC_API_KEY" in env_vars or os.environ.get("ANTHROPIC_API_KEY"):
211
+ try:
212
+ import anthropic
213
+
214
+ def fetch_anthropic_models():
215
+ client = anthropic.Anthropic(api_key=env_vars.get("ANTHROPIC_API_KEY") or os.environ.get("ANTHROPIC_API_KEY"))
216
+
217
+
218
+ return client.models.list()
219
+
220
+ future = executor.submit(fetch_anthropic_models)
221
+ models = future.result(timeout=timeout_seconds)
222
+
223
+ for model in models.data:
224
+ available_models[model.id] = 'anthropic'
225
+
226
+ except (ImportError, concurrent.futures.TimeoutError, Exception) as e:
227
+ logging.info(f"Anthropic models not indexed or timed out: {e}")
228
+
229
+ if "OPENAI_API_KEY" in env_vars or os.environ.get("OPENAI_API_KEY"):
230
+ try:
231
+ import openai
232
+
233
+ def fetch_openai_models():
234
+ openai.api_key = env_vars.get("OPENAI_API_KEY", None) or os.environ.get("OPENAI_API_KEY", None)
235
+ return openai.models.list()
236
+
237
+ future = executor.submit(fetch_openai_models)
238
+ models = future.result(timeout=timeout_seconds)
239
+
240
+ for model in models.data:
241
+ if (
242
+ (
243
+ "gpt" in model.id
244
+ or "o1" in model.id
245
+ or "o3" in model.id
246
+ or "chat" in model.id
247
+ )
248
+ and "audio" not in model.id
249
+ and "realtime" not in model.id
250
+ ):
251
+ available_models[model.id] = "openai"
252
+ except (ImportError, openai.APIError, concurrent.futures.TimeoutError, Exception) as e:
253
+ logging.info(f"OpenAI models not indexed or timed out: {e}")
254
+
255
+ if "GEMINI_API_KEY" in env_vars or os.environ.get("GEMINI_API_KEY"):
256
+ try:
257
+ from google import genai
258
+ def fetch_gemini_models():
259
+ client = genai.Client(api_key=env_vars.get("GEMINI_API_KEY") or os.environ.get("GEMINI_API_KEY"))
260
+ found_models = []
261
+
262
+ target_models = [
263
+ 'gemini-2.5-pro',
264
+ 'gemini-2.5-flash',
265
+ 'gemini-2.0-flash',
266
+ 'gemini-2.0-pro',
267
+ 'gemini-1.5-pro',
268
+ 'gemini-1.5-flash'
269
+ ]
270
+
271
+ for m in client.models.list():
272
+ for action in m.supported_actions:
273
+ if action == "generateContent":
274
+ if 'models/' in m.name:
275
+ model_name_part = m.name.split('/')[1]
276
+
277
+ if any(model in model_name_part for model in target_models):
278
+ found_models.append(model_name_part)
279
+ return set(found_models)
280
+ future = executor.submit(fetch_gemini_models)
281
+ models = future.result(timeout=timeout_seconds)
282
+
283
+ for model in models:
284
+ if "gemini" in model:
285
+ available_models[model] = "gemini"
286
+ except (ImportError, concurrent.futures.TimeoutError, Exception) as e:
287
+ logging.info(f"Gemini models not indexed or timed out: {e}")
288
+
289
+ if "DEEPSEEK_API_KEY" in env_vars or os.environ.get("DEEPSEEK_API_KEY"):
290
+ available_models['deepseek-chat'] = 'deepseek'
291
+ available_models['deepseek-reasoner'] = 'deepseek'
173
292
  try:
174
293
  import ollama
175
- models = ollama.list()
176
- for model in models.models:
294
+ timeout_seconds = 0.5
295
+ with concurrent.futures.ThreadPoolExecutor(max_workers=1) as ollama_executor:
296
+ def fetch_ollama_models():
297
+ return ollama.list()
298
+
299
+ future = ollama_executor.submit(fetch_ollama_models)
300
+ models = future.result(timeout=timeout_seconds)
177
301
 
302
+ for model in models.models:
178
303
  if "embed" not in model.model:
179
304
  mod = model.model
180
305
  available_models[mod] = "ollama"
181
- except Exception as e:
182
- print(f"Error loading ollama models: {e}")
183
- #print("locally available models", available_models)
306
+ except (ImportError, concurrent.futures.TimeoutError, Exception) as e:
307
+ logging.info(f"Error loading Ollama models or timed out: {e}")
184
308
 
185
309
  return available_models
186
310
 
187
311
 
188
- def validate_bash_command(command_parts: list) -> bool:
189
- """
190
- Function Description:
191
- Validate if the command sequence is a valid bash command with proper arguments/flags.
192
- Args:
193
- command_parts : list : Command parts
194
- Keyword Args:
195
- None
196
- Returns:
197
- bool : bool : Boolean
198
- """
199
- if not command_parts:
200
- return False
201
-
202
- COMMAND_PATTERNS = {
203
- "cat": {
204
- "flags": ["-n", "-b", "-E", "-T", "-s", "--number", "-A", "--show-all"],
205
- "requires_arg": True,
206
- },
207
- "find": {
208
- "flags": [
209
- "-name",
210
- "-type",
211
- "-size",
212
- "-mtime",
213
- "-exec",
214
- "-print",
215
- "-delete",
216
- "-maxdepth",
217
- "-mindepth",
218
- "-perm",
219
- "-user",
220
- "-group",
221
- ],
222
- "requires_arg": True,
223
- },
224
- "who": {
225
- "flags": [
226
- "-a",
227
- "-b",
228
- "-d",
229
- "-H",
230
- "-l",
231
- "-p",
232
- "-q",
233
- "-r",
234
- "-s",
235
- "-t",
236
- "-u",
237
- "--all",
238
- "--count",
239
- "--heading",
240
- ],
241
- "requires_arg": True,
242
- },
243
- "open": {
244
- "flags": ["-a", "-e", "-t", "-f", "-F", "-W", "-n", "-g", "-h"],
245
- "requires_arg": True,
246
- },
247
- "which": {"flags": ["-a", "-s", "-v"], "requires_arg": True},
248
- }
249
-
250
- base_command = command_parts[0]
251
-
252
- if base_command not in COMMAND_PATTERNS:
253
- return True # Allow other commands to pass through
254
-
255
- pattern = COMMAND_PATTERNS[base_command]
256
- args = []
257
- flags = []
258
-
259
- for i in range(1, len(command_parts)):
260
- part = command_parts[i]
261
- if part.startswith("-"):
262
- flags.append(part)
263
- if part not in pattern["flags"]:
264
- return False # Invalid flag
265
- else:
266
- args.append(part)
267
-
268
- # Check if 'who' has any arguments (it shouldn't)
269
- if base_command == "who" and args:
270
- return False
271
-
272
- # Handle 'which' with '-a' flag
273
- if base_command == "which" and "-a" in flags:
274
- return True # Allow 'which -a' with or without arguments.
275
-
276
- # Check if any required arguments are missing
277
- if pattern.get("requires_arg", False) and not args:
278
- return False
279
-
280
- return True
281
-
282
-
283
-
284
312
 
285
313
  def log_action(action: str, detail: str = "") -> None:
286
314
  """
@@ -298,67 +326,6 @@ def log_action(action: str, detail: str = "") -> None:
298
326
 
299
327
 
300
328
 
301
- def start_interactive_session(command: list) -> int:
302
- """
303
- Starts an interactive session. Only works on Unix. On Windows, print a message and return 1.
304
- """
305
- if ON_WINDOWS or termios is None or tty is None or pty is None or select is None or signal is None:
306
- print("Interactive terminal sessions are not supported on Windows.")
307
- return 1
308
- # Save the current terminal settings
309
- old_tty = termios.tcgetattr(sys.stdin)
310
- try:
311
- # Create a pseudo-terminal
312
- master_fd, slave_fd = pty.openpty()
313
-
314
- # Start the process
315
- p = subprocess.Popen(
316
- command,
317
- stdin=slave_fd,
318
- stdout=slave_fd,
319
- stderr=slave_fd,
320
- shell=True,
321
- preexec_fn=os.setsid, # Create a new process group
322
- )
323
-
324
- # Set the terminal to raw mode
325
- tty.setraw(sys.stdin.fileno())
326
-
327
- def handle_timeout(signum, frame):
328
- raise TimeoutError("Process did not terminate in time")
329
-
330
- while p.poll() is None:
331
- r, w, e = select.select([sys.stdin, master_fd], [], [], 0.1)
332
- if sys.stdin in r:
333
- d = os.read(sys.stdin.fileno(), 10240)
334
- os.write(master_fd, d)
335
- elif master_fd in r:
336
- o = os.read(master_fd, 10240)
337
- if o:
338
- os.write(sys.stdout.fileno(), o)
339
- else:
340
- break
341
-
342
- # Wait for the process to terminate with a timeout
343
- signal.signal(signal.SIGALRM, handle_timeout)
344
- signal.alarm(5) # 5 second timeout
345
- try:
346
- p.wait()
347
- except TimeoutError:
348
- print("\nProcess did not terminate. Force killing...")
349
- os.killpg(os.getpgid(p.pid), signal.SIGTERM)
350
- time.sleep(1)
351
- if p.poll() is None:
352
- os.killpg(os.getpgid(p.pid), signal.SIGKILL)
353
- finally:
354
- signal.alarm(0)
355
-
356
- finally:
357
- # Restore the terminal settings
358
- termios.tcsetattr(sys.stdin, termios.TCSAFLUSH, old_tty)
359
-
360
- return p.returncode
361
-
362
329
  def preprocess_code_block(code_text):
363
330
  """
364
331
  Preprocess code block text to remove leading spaces.
@@ -378,9 +345,9 @@ def preprocess_markdown(md_text):
378
345
  current_code_block = []
379
346
 
380
347
  for line in lines:
381
- if line.startswith("```"): # Toggle code block
348
+ if line.startswith("```"):
382
349
  if inside_code_block:
383
- # Close code block, unindent, and append
350
+
384
351
  processed_lines.append("```")
385
352
  processed_lines.extend(
386
353
  textwrap.dedent("\n".join(current_code_block)).split("\n")
@@ -396,159 +363,6 @@ def preprocess_markdown(md_text):
396
363
  return "\n".join(processed_lines)
397
364
 
398
365
 
399
- BASH_COMMANDS = [
400
- "npc",
401
- "npm",
402
- "npx",
403
- "open",
404
- "alias",
405
- "bg",
406
- "bind",
407
- "break",
408
- "builtin",
409
- "case",
410
- "command",
411
- "compgen",
412
- "complete",
413
- "continue",
414
- "declare",
415
- "dirs",
416
- "disown",
417
- "echo",
418
- "enable",
419
- "eval",
420
- "exec",
421
- "exit",
422
- "export",
423
- "fc",
424
- "fg",
425
- "getopts",
426
- "hash",
427
- "help",
428
- "history",
429
- "if",
430
- "jobs",
431
- "kill",
432
- "let",
433
- "local",
434
- "logout",
435
- "ollama",
436
- "popd",
437
- "printf",
438
- "pushd",
439
- "pwd",
440
- "read",
441
- "readonly",
442
- "return",
443
- "set",
444
- "shift",
445
- "shopt",
446
- "source",
447
- "suspend",
448
- "test",
449
- "times",
450
- "trap",
451
- "type",
452
- "typeset",
453
- "ulimit",
454
- "umask",
455
- "unalias",
456
- "unset",
457
- "until",
458
- "wait",
459
- "while",
460
- # Common Unix commands
461
- "ls",
462
- "cp",
463
- "mv",
464
- "rm",
465
- "mkdir",
466
- "rmdir",
467
- "touch",
468
- "cat",
469
- "less",
470
- "more",
471
- "head",
472
- "tail",
473
- "grep",
474
- "find",
475
- "sed",
476
- "awk",
477
- "sort",
478
- "uniq",
479
- "wc",
480
- "diff",
481
- "chmod",
482
- "chown",
483
- "chgrp",
484
- "ln",
485
- "tar",
486
- "gzip",
487
- "gunzip",
488
- "zip",
489
- "unzip",
490
- "ssh",
491
- "scp",
492
- "rsync",
493
- "wget",
494
- "curl",
495
- "ping",
496
- "netstat",
497
- "ifconfig",
498
- "route",
499
- "traceroute",
500
- "ps",
501
- "top",
502
- "htop",
503
- "kill",
504
- "killall",
505
- "su",
506
- "sudo",
507
- "whoami",
508
- "who",
509
- "w",
510
- "last",
511
- "finger",
512
- "uptime",
513
- "free",
514
- "df",
515
- "du",
516
- "mount",
517
- "umount",
518
- "fdisk",
519
- "mkfs",
520
- "fsck",
521
- "dd",
522
- "cron",
523
- "at",
524
- "systemctl",
525
- "service",
526
- "journalctl",
527
- "man",
528
- "info",
529
- "whatis",
530
- "whereis",
531
- "which",
532
- "date",
533
- "cal",
534
- "bc",
535
- "expr",
536
- "screen",
537
- "tmux",
538
- "git",
539
- "vim",
540
- "emacs",
541
- "nano",
542
- "pip",
543
- ]
544
-
545
-
546
- interactive_commands = {
547
- "ipython": ["ipython"],
548
- "python": ["python", "-i"],
549
- "sqlite3": ["sqlite3"],
550
- "r": ["R", "--interactive"],
551
- }
552
366
 
553
367
  def request_user_input(input_request: Dict[str, str]) -> str:
554
368
  """
@@ -578,7 +392,7 @@ def render_markdown(text: str) -> None:
578
392
  for line in lines:
579
393
  if line.startswith("```"):
580
394
  if inside_code_block:
581
- # End of code block - render the collected code
395
+
582
396
  code = "\n".join(code_lines)
583
397
  if code.strip():
584
398
  syntax = Syntax(
@@ -587,252 +401,15 @@ def render_markdown(text: str) -> None:
587
401
  console.print(syntax)
588
402
  code_lines = []
589
403
  else:
590
- # Start of code block - get language if specified
404
+
591
405
  lang = line[3:].strip() or None
592
406
  inside_code_block = not inside_code_block
593
407
  elif inside_code_block:
594
408
  code_lines.append(line)
595
409
  else:
596
- # Regular markdown
410
+
597
411
  console.print(Markdown(line))
598
412
 
599
-
600
-
601
-
602
-
603
-
604
- def execute_set_command(command: str, value: str) -> str:
605
- """
606
- Function Description:
607
- This function sets a configuration value in the .npcshrc file.
608
- Args:
609
- command: The command to execute.
610
- value: The value to set.
611
- Keyword Args:
612
- None
613
- Returns:
614
- A message indicating the success or failure of the operation.
615
- """
616
-
617
- config_path = os.path.expanduser("~/.npcshrc")
618
-
619
- # Map command to environment variable name
620
- var_map = {
621
- "model": "NPCSH_CHAT_MODEL",
622
- "provider": "NPCSH_CHAT_PROVIDER",
623
- "db_path": "NPCSH_DB_PATH",
624
- }
625
-
626
- if command not in var_map:
627
- return f"Unknown setting: {command}"
628
-
629
- env_var = var_map[command]
630
-
631
- # Read the current configuration
632
- if os.path.exists(config_path):
633
- with open(config_path, "r") as f:
634
- lines = f.readlines()
635
- else:
636
- lines = []
637
-
638
- # Check if the property exists and update it, or add it if it doesn't exist
639
- property_exists = False
640
- for i, line in enumerate(lines):
641
- if line.startswith(f"export {env_var}="):
642
- lines[i] = f"export {env_var}='{value}'\n"
643
- property_exists = True
644
- break
645
-
646
- if not property_exists:
647
- lines.append(f"export {env_var}='{value}'\n")
648
-
649
- # Save the updated configuration
650
- with open(config_path, "w") as f:
651
- f.writelines(lines)
652
-
653
- return f"{command.capitalize()} has been set to: {value}"
654
-
655
- # Function to check and download NLTK data if necessary
656
- def ensure_nltk_punkt() -> None:
657
- """
658
- Function Description:
659
- This function ensures that the NLTK 'punkt' tokenizer is downloaded.
660
- Args:
661
- None
662
- Keyword Args:
663
- None
664
- Returns:
665
- None
666
- """
667
-
668
- try:
669
- nltk.data.find("tokenizers/punkt")
670
- except LookupError:
671
- print("Downloading NLTK 'punkt' tokenizer...")
672
- nltk.download("punkt")
673
-
674
- def get_shell_config_file() -> str:
675
- """
676
-
677
- Function Description:
678
- This function returns the path to the shell configuration file.
679
- Args:
680
- None
681
- Keyword Args:
682
- None
683
- Returns:
684
- The path to the shell configuration file.
685
- """
686
- # Check the current shell
687
- shell = os.environ.get("SHELL", "")
688
-
689
- if "zsh" in shell:
690
- return os.path.expanduser("~/.zshrc")
691
- elif "bash" in shell:
692
- # On macOS, use .bash_profile for login shells
693
- if platform.system() == "Darwin":
694
- return os.path.expanduser("~/.bash_profile")
695
- else:
696
- return os.path.expanduser("~/.bashrc")
697
- else:
698
- # Default to .bashrc if we can't determine the shell
699
- return os.path.expanduser("~/.bashrc")
700
-
701
-
702
-
703
- def add_npcshrc_to_shell_config() -> None:
704
- """
705
- Function Description:
706
- This function adds the sourcing of the .npcshrc file to the user's shell configuration file.
707
- Args:
708
- None
709
- Keyword Args:
710
- None
711
- Returns:
712
- None
713
- """
714
-
715
- if os.getenv("NPCSH_INITIALIZED") is not None:
716
- return
717
- config_file = get_shell_config_file()
718
- npcshrc_line = "\n# Source NPCSH configuration\nif [ -f ~/.npcshrc ]; then\n . ~/.npcshrc\nfi\n"
719
-
720
- with open(config_file, "a+") as shell_config:
721
- shell_config.seek(0)
722
- content = shell_config.read()
723
- if "source ~/.npcshrc" not in content and ". ~/.npcshrc" not in content:
724
- shell_config.write(npcshrc_line)
725
- print(f"Added .npcshrc sourcing to {config_file}")
726
- else:
727
- print(f".npcshrc already sourced in {config_file}")
728
-
729
- def ensure_npcshrc_exists() -> str:
730
- """
731
- Function Description:
732
- This function ensures that the .npcshrc file exists in the user's home directory.
733
- Args:
734
- None
735
- Keyword Args:
736
- None
737
- Returns:
738
- The path to the .npcshrc file.
739
- """
740
-
741
- npcshrc_path = os.path.expanduser("~/.npcshrc")
742
- if not os.path.exists(npcshrc_path):
743
- with open(npcshrc_path, "w") as npcshrc:
744
- npcshrc.write("# NPCSH Configuration File\n")
745
- npcshrc.write("export NPCSH_INITIALIZED=0\n")
746
- npcshrc.write("export NPCSH_DEFAULT_MODE='agent'\n")
747
- npcshrc.write("export NPCSH_CHAT_PROVIDER='ollama'\n")
748
- npcshrc.write("export NPCSH_CHAT_MODEL='llama3.2'\n")
749
- npcshrc.write("export NPCSH_REASONING_PROVIDER='ollama'\n")
750
- npcshrc.write("export NPCSH_REASONING_MODEL='deepseek-r1'\n")
751
-
752
- npcshrc.write("export NPCSH_EMBEDDING_PROVIDER='ollama'\n")
753
- npcshrc.write("export NPCSH_EMBEDDING_MODEL='nomic-embed-text'\n")
754
- npcshrc.write("export NPCSH_VISION_PROVIDER='ollama'\n")
755
- npcshrc.write("export NPCSH_VISION_MODEL='llava7b'\n")
756
- npcshrc.write(
757
- "export NPCSH_IMAGE_GEN_MODEL='runwayml/stable-diffusion-v1-5'\n"
758
- )
759
-
760
- npcshrc.write("export NPCSH_IMAGE_GEN_PROVIDER='diffusers'\n")
761
- npcshrc.write(
762
- "export NPCSH_VIDEO_GEN_MODEL='runwayml/stable-diffusion-v1-5'\n"
763
- )
764
-
765
- npcshrc.write("export NPCSH_VIDEO_GEN_PROVIDER='diffusers'\n")
766
-
767
- npcshrc.write("export NPCSH_API_URL=''\n")
768
- npcshrc.write("export NPCSH_DB_PATH='~/npcsh_history.db'\n")
769
- npcshrc.write("export NPCSH_VECTOR_DB_PATH='~/npcsh_chroma.db'\n")
770
- npcshrc.write("export NPCSH_STREAM_OUTPUT=0")
771
- return npcshrc_path
772
-
773
-
774
-
775
- def setup_npcsh_config() -> None:
776
- """
777
- Function Description:
778
- This function initializes the NPCSH configuration.
779
- Args:
780
- None
781
- Keyword Args:
782
- None
783
- Returns:
784
- None
785
- """
786
-
787
- ensure_npcshrc_exists()
788
- add_npcshrc_to_shell_config()
789
-
790
-
791
- def is_npcsh_initialized() -> bool:
792
- """
793
- Function Description:
794
- This function checks if the NPCSH initialization flag is set.
795
- Args:
796
- None
797
- Keyword Args:
798
- None
799
- Returns:
800
- A boolean indicating whether NPCSH is initialized.
801
- """
802
-
803
- return os.environ.get("NPCSH_INITIALIZED", None) == "1"
804
-
805
-
806
- def set_npcsh_initialized() -> None:
807
- """
808
- Function Description:
809
- This function sets the NPCSH initialization flag in the .npcshrc file.
810
- Args:
811
- None
812
- Keyword Args:
813
- None
814
- Returns:
815
-
816
- None
817
- """
818
-
819
- npcshrc_path = ensure_npcshrc_exists()
820
-
821
- with open(npcshrc_path, "r+") as npcshrc:
822
- content = npcshrc.read()
823
- if "export NPCSH_INITIALIZED=0" in content:
824
- content = content.replace(
825
- "export NPCSH_INITIALIZED=0", "export NPCSH_INITIALIZED=1"
826
- )
827
- npcshrc.seek(0)
828
- npcshrc.write(content)
829
- npcshrc.truncate()
830
-
831
- # Also set it for the current session
832
- os.environ["NPCSH_INITIALIZED"] = "1"
833
- print("NPCSH initialization flag set in .npcshrc")
834
-
835
-
836
413
  def get_directory_npcs(directory: str = None) -> List[str]:
837
414
  """
838
415
  Function Description:
@@ -873,301 +450,6 @@ def get_db_npcs(db_path: str) -> List[str]:
873
450
  db_conn.close()
874
451
  return npcs
875
452
 
876
-
877
- def get_npc_path(npc_name: str, db_path: str) -> str:
878
- # First, check in project npc_team directory
879
- project_npc_team_dir = os.path.abspath("./npc_team")
880
- project_npc_path = os.path.join(project_npc_team_dir, f"{npc_name}.npc")
881
-
882
- # Then, check in global npc_team directory
883
- user_npc_team_dir = os.path.expanduser("~/.npcsh/npc_team")
884
- global_npc_path = os.path.join(user_npc_team_dir, f"{npc_name}.npc")
885
-
886
- # Check database for compiled NPCs
887
- try:
888
- with sqlite3.connect(db_path) as conn:
889
- cursor = conn.cursor()
890
- query = f"SELECT source_path FROM compiled_npcs WHERE name = '{npc_name}'"
891
- cursor.execute(query)
892
- result = cursor.fetchone()
893
- if result:
894
- return result[0]
895
-
896
- except Exception as e:
897
- try:
898
- with sqlite3.connect(db_path) as conn:
899
- cursor = conn.cursor()
900
- query = f"SELECT source_path FROM compiled_npcs WHERE name = {npc_name}"
901
- cursor.execute(query)
902
- result = cursor.fetchone()
903
- if result:
904
- return result[0]
905
- except Exception as e:
906
- print(f"Database query error: {e}")
907
-
908
- # Fallback to file paths
909
- if os.path.exists(project_npc_path):
910
- return project_npc_path
911
-
912
- if os.path.exists(global_npc_path):
913
- return global_npc_path
914
-
915
- raise ValueError(f"NPC file not found: {npc_name}")
916
-
917
-
918
- def initialize_base_npcs_if_needed(db_path: str) -> None:
919
- """
920
- Function Description:
921
- This function initializes the base NPCs if they are not already in the database.
922
- Args:
923
- db_path: The path to the database file.
924
- Keyword Args:
925
-
926
- None
927
- Returns:
928
- None
929
- """
930
-
931
- if is_npcsh_initialized():
932
- return
933
-
934
- conn = sqlite3.connect(db_path)
935
- cursor = conn.cursor()
936
-
937
- # Create the compiled_npcs table if it doesn't exist
938
- cursor.execute(
939
- """
940
- CREATE TABLE IF NOT EXISTS compiled_npcs (
941
- name TEXT PRIMARY KEY,
942
- source_path TEXT NOT NULL,
943
- compiled_content TEXT
944
- )
945
- """
946
- )
947
-
948
- # Get the path to the npc_team directory in the package
949
- package_dir = os.path.dirname(__file__)
950
- package_npc_team_dir = os.path.join(package_dir, "npc_team")
951
-
952
-
953
-
954
- # User's global npc_team directory
955
- user_npc_team_dir = os.path.expanduser("~/.npcsh/npc_team")
956
-
957
- user_jinxs_dir = os.path.join(user_npc_team_dir, "jinxs")
958
- user_templates_dir = os.path.join(user_npc_team_dir, "templates")
959
- os.makedirs(user_npc_team_dir, exist_ok=True)
960
- os.makedirs(user_jinxs_dir, exist_ok=True)
961
- os.makedirs(user_templates_dir, exist_ok=True)
962
- # Copy NPCs from package to user directory
963
- for filename in os.listdir(package_npc_team_dir):
964
- if filename.endswith(".npc"):
965
- source_path = os.path.join(package_npc_team_dir, filename)
966
- destination_path = os.path.join(user_npc_team_dir, filename)
967
- if not os.path.exists(destination_path) or file_has_changed(
968
- source_path, destination_path
969
- ):
970
- shutil.copy2(source_path, destination_path)
971
- print(f"Copied NPC {filename} to {destination_path}")
972
- if filename.endswith(".ctx"):
973
- source_path = os.path.join(package_npc_team_dir, filename)
974
- destination_path = os.path.join(user_npc_team_dir, filename)
975
- if not os.path.exists(destination_path) or file_has_changed(
976
- source_path, destination_path
977
- ):
978
- shutil.copy2(source_path, destination_path)
979
- print(f"Copied ctx {filename} to {destination_path}")
980
-
981
- # Copy jinxs from package to user directory
982
- package_jinxs_dir = os.path.join(package_npc_team_dir, "jinxs")
983
- if os.path.exists(package_jinxs_dir):
984
- for filename in os.listdir(package_jinxs_dir):
985
- if filename.endswith(".jinx"):
986
- source_jinx_path = os.path.join(package_jinxs_dir, filename)
987
- destination_jinx_path = os.path.join(user_jinxs_dir, filename)
988
- if (not os.path.exists(destination_jinx_path)) or file_has_changed(
989
- source_jinx_path, destination_jinx_path
990
- ):
991
- shutil.copy2(source_jinx_path, destination_jinx_path)
992
- print(f"Copied jinx {filename} to {destination_jinx_path}")
993
-
994
- templates = os.path.join(package_npc_team_dir, "templates")
995
- if os.path.exists(templates):
996
- for folder in os.listdir(templates):
997
- os.makedirs(os.path.join(user_templates_dir, folder), exist_ok=True)
998
- for file in os.listdir(os.path.join(templates, folder)):
999
- if file.endswith(".npc"):
1000
- source_template_path = os.path.join(templates, folder, file)
1001
-
1002
- destination_template_path = os.path.join(
1003
- user_templates_dir, folder, file
1004
- )
1005
- if not os.path.exists(
1006
- destination_template_path
1007
- ) or file_has_changed(
1008
- source_template_path, destination_template_path
1009
- ):
1010
- shutil.copy2(source_template_path, destination_template_path)
1011
- print(f"Copied template {file} to {destination_template_path}")
1012
- conn.commit()
1013
- conn.close()
1014
- set_npcsh_initialized()
1015
- add_npcshrc_to_shell_config()
1016
-
1017
-
1018
- def file_has_changed(source_path: str, destination_path: str) -> bool:
1019
- """
1020
- Function Description:
1021
- This function compares two files to determine if they are different.
1022
- Args:
1023
- source_path: The path to the source file.
1024
- destination_path: The path to the destination file.
1025
- Keyword Args:
1026
- None
1027
- Returns:
1028
- A boolean indicating whether the files are different
1029
- """
1030
-
1031
- # Compare file modification times or contents to decide whether to update the file
1032
- return not filecmp.cmp(source_path, destination_path, shallow=False)
1033
-
1034
-
1035
- def is_valid_npc(npc: str, db_path: str) -> bool:
1036
- """
1037
- Function Description:
1038
- This function checks if an NPC is valid based on the database.
1039
- Args:
1040
- npc: The name of the NPC.
1041
- db_path: The path to the database file.
1042
- Keyword Args:
1043
- None
1044
- Returns:
1045
- A boolean indicating whether the NPC is valid.
1046
- """
1047
-
1048
- conn = sqlite3.connect(db_path)
1049
- cursor = conn.cursor()
1050
- cursor.execute("SELECT * FROM compiled_npcs WHERE name = ?", (npc,))
1051
- result = cursor.fetchone()
1052
- conn.close()
1053
- return result is not None
1054
-
1055
-
1056
- def execute_python(code: str) -> str:
1057
- """
1058
- Function Description:
1059
- This function executes Python code and returns the output.
1060
- Args:
1061
- code: The Python code to execute.
1062
- Keyword Args:
1063
- None
1064
- Returns:
1065
- The output of the code execution.
1066
- """
1067
-
1068
- try:
1069
- result = subprocess.run(
1070
- ["python", "-c", code], capture_output=True, text=True, timeout=30
1071
- )
1072
- return result.stdout if result.returncode == 0 else f"Error: {result.stderr}"
1073
- except subprocess.TimeoutExpired:
1074
- return "Error: Execution timed out"
1075
-
1076
-
1077
- def execute_r(code: str) -> str:
1078
- """
1079
- Function Description:
1080
- This function executes R code and returns the output.
1081
- Args:
1082
- code: The R code to execute.
1083
- Keyword Args:
1084
- None
1085
- Returns:
1086
- The output of the code execution.
1087
- """
1088
-
1089
- try:
1090
- with tempfile.NamedTemporaryFile(
1091
- mode="w", suffix=".R", delete=False
1092
- ) as temp_file:
1093
- temp_file.write(code)
1094
- temp_file_path = temp_file.name
1095
-
1096
- result = subprocess.run(
1097
- ["Rscript", temp_file_path], capture_output=True, text=True, timeout=30
1098
- )
1099
- os.unlink(temp_file_path)
1100
- return result.stdout if result.returncode == 0 else f"Error: {result.stderr}"
1101
- except subprocess.TimeoutExpired:
1102
- os.unlink(temp_file_path)
1103
- return "Error: Execution timed out"
1104
-
1105
-
1106
- def execute_sql(code: str) -> str:
1107
- """
1108
- Function Description:
1109
- This function executes SQL code and returns the output.
1110
- Args:
1111
- code: The SQL code to execute.
1112
- Keyword Args:
1113
- None
1114
- Returns:
1115
- result: The output of the code execution.
1116
- """
1117
- # use pandas to run the sql
1118
- try:
1119
- result = pd.read_sql_query(code, con=sqlite3.connect("npcsh_history.db"))
1120
- return result
1121
- except Exception as e:
1122
- return f"Error: {e}"
1123
-
1124
-
1125
- def list_directory(args: List[str]) -> None:
1126
- """
1127
- Function Description:
1128
- This function lists the contents of a directory.
1129
- Args:
1130
- args: The command arguments.
1131
- Keyword Args:
1132
- None
1133
- Returns:
1134
- None
1135
- """
1136
- directory = args[0] if args else "."
1137
- try:
1138
- files = os.listdir(directory)
1139
- for f in files:
1140
- print(f)
1141
- except Exception as e:
1142
- print(f"Error listing directory: {e}")
1143
-
1144
-
1145
- def read_file(args: List[str]) -> None:
1146
- """
1147
- Function Description:
1148
- This function reads the contents of a file.
1149
- Args:
1150
- args: The command arguments.
1151
- Keyword Args:
1152
- None
1153
- Returns:
1154
- None
1155
- """
1156
-
1157
- if not args:
1158
- print("Usage: /read <filename>")
1159
- return
1160
- filename = args[0]
1161
- try:
1162
- with open(filename, "r") as file:
1163
- content = file.read()
1164
- print(content)
1165
- except Exception as e:
1166
- print(f"Error reading file: {e}")
1167
-
1168
-
1169
-
1170
-
1171
453
  def guess_mime_type(filename):
1172
454
  """Guess the MIME type of a file based on its extension."""
1173
455
  extension = os.path.splitext(filename)[1].lower()
@@ -1186,42 +468,6 @@ def guess_mime_type(filename):
1186
468
  }
1187
469
  return mime_types.get(extension, "application/octet-stream")
1188
470
 
1189
- import os
1190
- import json
1191
- from pathlib import Path
1192
-
1193
-
1194
- def change_directory(command_parts: list, messages: list) -> dict:
1195
- """
1196
- Function Description:
1197
- Changes the current directory.
1198
- Args:
1199
- command_parts : list : Command parts
1200
- messages : list : Messages
1201
- Keyword Args:
1202
- None
1203
- Returns:
1204
- dict : dict : Dictionary
1205
-
1206
- """
1207
-
1208
- try:
1209
- if len(command_parts) > 1:
1210
- new_dir = os.path.expanduser(command_parts[1])
1211
- else:
1212
- new_dir = os.path.expanduser("~")
1213
- os.chdir(new_dir)
1214
- return {
1215
- "messages": messages,
1216
- "output": f"Changed directory to {os.getcwd()}",
1217
- }
1218
- except FileNotFoundError:
1219
- return {
1220
- "messages": messages,
1221
- "output": f"Directory not found: {new_dir}",
1222
- }
1223
- except PermissionError:
1224
- return {"messages": messages, "output": f"Permission denied: {new_dir}"}
1225
471
 
1226
472
  def ensure_dirs_exist(*dirs):
1227
473
  """Ensure all specified directories exist"""
@@ -1232,7 +478,7 @@ def init_db_tables(db_path="~/npcsh_history.db"):
1232
478
  """Initialize necessary database tables"""
1233
479
  db_path = os.path.expanduser(db_path)
1234
480
  with sqlite3.connect(db_path) as conn:
1235
- # NPC log table for storing all kinds of entries
481
+
1236
482
  conn.execute("""
1237
483
  CREATE TABLE IF NOT EXISTS npc_log (
1238
484
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -1244,7 +490,7 @@ def init_db_tables(db_path="~/npcsh_history.db"):
1244
490
  )
1245
491
  """)
1246
492
 
1247
- # Pipeline runs table for tracking pipeline executions
493
+
1248
494
  conn.execute("""
1249
495
  CREATE TABLE IF NOT EXISTS pipeline_runs (
1250
496
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -1255,7 +501,7 @@ def init_db_tables(db_path="~/npcsh_history.db"):
1255
501
  )
1256
502
  """)
1257
503
 
1258
- # Compiled NPCs table for storing compiled NPC content
504
+
1259
505
  conn.execute("""
1260
506
  CREATE TABLE IF NOT EXISTS compiled_npcs (
1261
507
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -1271,51 +517,6 @@ def init_db_tables(db_path="~/npcsh_history.db"):
1271
517
 
1272
518
 
1273
519
 
1274
- def orange(text: str) -> str:
1275
- """
1276
- Function Description:
1277
- Returns orange text.
1278
- Args:
1279
- text : str : Text
1280
- Keyword Args:
1281
- None
1282
- Returns:
1283
- text : str : Text
1284
-
1285
- """
1286
- return f"\033[38;2;255;165;0m{text}{Style.RESET_ALL}"
1287
-
1288
- def get_npcshrc_path_windows():
1289
- return Path.home() / ".npcshrc"
1290
-
1291
-
1292
- def read_rc_file_windows(path):
1293
- """Read shell-style rc file"""
1294
- config = {}
1295
- if not path.exists():
1296
- return config
1297
-
1298
- with open(path) as f:
1299
- for line in f:
1300
- line = line.strip()
1301
- if line and not line.startswith("#"):
1302
- # Match KEY='value' or KEY="value" format
1303
- match = re.match(r'^([A-Z_]+)\s*=\s*[\'"](.*?)[\'"]$', line)
1304
- if match:
1305
- key, value = match.groups()
1306
- config[key] = value
1307
- return config
1308
-
1309
-
1310
- def get_setting_windows(key, default=None):
1311
- # Try environment variable first
1312
- if env_value := os.getenv(key):
1313
- return env_value
1314
-
1315
- # Fall back to .npcshrc file
1316
- config = read_rc_file_windows(get_npcshrc_path_windows())
1317
- return config.get(key, default)
1318
-
1319
520
  def get_model_and_provider(command: str, available_models: list) -> tuple:
1320
521
  """
1321
522
  Function Description:
@@ -1336,26 +537,26 @@ def get_model_and_provider(command: str, available_models: list) -> tuple:
1336
537
  model_match = re.search(r"@(\S+)", command)
1337
538
  if model_match:
1338
539
  model_name = model_match.group(1)
1339
- # Autocomplete model name
540
+
1340
541
  matches = [m for m in available_models if m.startswith(model_name)]
1341
542
  if matches:
1342
543
  if len(matches) == 1:
1343
- model_name = matches[0] # Complete the name if only one match
1344
- # Find provider for the (potentially autocompleted) model
544
+ model_name = matches[0]
545
+
1345
546
  provider = lookup_provider(model_name)
1346
547
  if provider:
1347
- # Remove the model tag from the command
548
+
1348
549
  cleaned_command = command.replace(
1349
550
  f"@{model_match.group(1)}", ""
1350
551
  ).strip()
1351
- # print(cleaned_command, 'cleaned_command')
552
+
1352
553
  return model_name, provider, cleaned_command
1353
554
  else:
1354
- return None, None, command # Provider not found
555
+ return None, None, command
1355
556
  else:
1356
- return None, None, command # No matching model
557
+ return None, None, command
1357
558
  else:
1358
- return None, None, command # No model specified
559
+ return None, None, command
1359
560
 
1360
561
  def render_code_block(code: str, language: str = None) -> None:
1361
562
  """Render a code block with syntax highlighting using rich, left-justified with no line numbers"""
@@ -1364,172 +565,305 @@ def render_code_block(code: str, language: str = None) -> None:
1364
565
 
1365
566
  console = Console(highlight=True)
1366
567
  code = code.strip()
1367
- # If code starts with a language identifier, remove it
568
+
1368
569
  if code.split("\n", 1)[0].lower() in ["python", "bash", "javascript"]:
1369
570
  code = code.split("\n", 1)[1]
1370
571
  syntax = Syntax(
1371
572
  code, language or "python", theme="monokai", line_numbers=False, padding=0
1372
573
  )
1373
574
  console.print(syntax)
1374
- def print_and_process_stream_with_markdown(response, model, provider):
575
+
576
+ def print_and_process_stream_with_markdown(response, model, provider, show=False):
577
+ import sys
578
+
1375
579
  str_output = ""
1376
- dot_count = 0 # Keep track of how many dots we've printed
580
+ dot_count = 0
1377
581
  tool_call_data = {"id": None, "function_name": None, "arguments": ""}
1378
-
1379
- for chunk in response:
1380
- # Get chunk content based on provider
1381
- print('.', end="", flush=True)
1382
- dot_count += 1
1383
-
1384
- # Extract tool call info based on provider
1385
- if provider == "ollama":
1386
- # Ollama tool call extraction
1387
- if "message" in chunk and "tool_calls" in chunk["message"]:
1388
- for tool_call in chunk["message"]["tool_calls"]:
1389
- if "id" in tool_call:
1390
- tool_call_data["id"] = tool_call["id"]
1391
- if "function" in tool_call:
1392
- if "name" in tool_call["function"]:
1393
- tool_call_data["function_name"] = tool_call["function"]["name"]
1394
- if "arguments" in tool_call["function"]:
1395
- tool_call_data["arguments"] += tool_call["function"]["arguments"]
1396
-
1397
- chunk_content = chunk["message"]["content"] if "message" in chunk and "content" in chunk["message"] else ""
1398
- else:
1399
- # LiteLLM tool call extraction
1400
- for c in chunk.choices:
1401
- if hasattr(c.delta, "tool_calls") and c.delta.tool_calls:
1402
- for tool_call in c.delta.tool_calls:
1403
- if tool_call.id:
1404
- tool_call_data["id"] = tool_call.id
1405
- if tool_call.function:
1406
- if hasattr(tool_call.function, "name") and tool_call.function.name:
1407
- tool_call_data["function_name"] = tool_call.function.name
1408
- if hasattr(tool_call.function, "arguments") and tool_call.function.arguments:
1409
- tool_call_data["arguments"] += tool_call.function.arguments
1410
-
1411
- chunk_content = ''
1412
- reasoning_content = ''
1413
- for c in chunk.choices:
1414
- if hasattr(c.delta, "reasoning_content"):
1415
- reasoning_content += c.delta.reasoning_content
1416
-
1417
- if reasoning_content:
1418
- chunk_content = reasoning_content
582
+ interrupted = False
583
+
584
+ if isinstance(response, str):
585
+ render_markdown(response)
586
+ print('\n')
587
+ return response
588
+
589
+
590
+ sys.stdout.write('\033[s')
591
+ sys.stdout.flush()
592
+
593
+ try:
594
+ for chunk in response:
595
+
596
+ if provider == "ollama" and 'gpt-oss' not in model:
597
+
598
+ if "message" in chunk and "tool_calls" in chunk["message"]:
599
+ for tool_call in chunk["message"]["tool_calls"]:
600
+ if "id" in tool_call:
601
+ tool_call_data["id"] = tool_call["id"]
602
+ if "function" in tool_call:
603
+ if "name" in tool_call["function"]:
604
+ tool_call_data["function_name"] = tool_call["function"]["name"]
605
+ if "arguments" in tool_call["function"]:
606
+ if isinstance(tool_call["function"]["arguments"], dict):
607
+ tool_call_data["arguments"] += json.dumps(tool_call["function"]["arguments"])
608
+ else:
609
+ tool_call_data["arguments"] += tool_call["function"]["arguments"]
610
+ chunk_content = chunk["message"]["content"] if "message" in chunk and "content" in chunk["message"] else ""
611
+ reasoning_content = chunk['message'].get('thinking', '') if "message" in chunk and "thinking" in chunk['message'] else ""
612
+ if show:
613
+ if len(reasoning_content) > 0:
614
+ print(reasoning_content, end="", flush=True)
615
+ if chunk_content != "":
616
+ print(chunk_content, end="", flush=True)
617
+ else:
618
+ print('.', end="", flush=True)
619
+ dot_count += 1
1419
620
 
1420
- chunk_content += "".join(
1421
- c.delta.content for c in chunk.choices if c.delta.content
1422
- )
1423
-
1424
- if not chunk_content:
1425
- continue
1426
- str_output += chunk_content
621
+ else:
622
+ for c in chunk.choices:
623
+ if hasattr(c.delta, "tool_calls") and c.delta.tool_calls:
624
+ for tool_call in c.delta.tool_calls:
625
+ if tool_call.id:
626
+ tool_call_data["id"] = tool_call.id
627
+ if tool_call.function:
628
+ if hasattr(tool_call.function, "name") and tool_call.function.name:
629
+ tool_call_data["function_name"] = tool_call.function.name
630
+ if hasattr(tool_call.function, "arguments") and tool_call.function.arguments:
631
+ tool_call_data["arguments"] += tool_call.function.arguments
632
+
633
+ chunk_content = ''
634
+ reasoning_content = ''
635
+ for c in chunk.choices:
636
+ if hasattr(c.delta, "reasoning_content"):
637
+ reasoning_content += c.delta.reasoning_content
638
+
639
+ chunk_content += "".join(
640
+ c.delta.content for c in chunk.choices if c.delta.content
641
+ )
642
+ if show:
643
+ if reasoning_content is not None:
644
+ print(reasoning_content, end="", flush=True)
645
+ if chunk_content != "":
646
+ print(chunk_content, end="", flush=True)
647
+ else:
648
+ print('.', end="", flush=True)
649
+ dot_count += 1
650
+
651
+ if not chunk_content:
652
+ continue
653
+ str_output += chunk_content
1427
654
 
1428
- # Clear the dots by returning to the start of line and printing spaces
1429
- print('\r' + ' ' * dot_count*2 + '\r', end="", flush=True)
655
+ except KeyboardInterrupt:
656
+ interrupted = True
657
+ print('\n⚠️ Stream interrupted by user')
1430
658
 
1431
- # Add tool call information to str_output if any was found
1432
659
  if tool_call_data["id"] or tool_call_data["function_name"] or tool_call_data["arguments"]:
1433
- str_output += "\n\n### Jinx Call Data\n"
660
+ str_output += "\n\n"
1434
661
  if tool_call_data["id"]:
1435
662
  str_output += f"**ID:** {tool_call_data['id']}\n\n"
1436
663
  if tool_call_data["function_name"]:
1437
664
  str_output += f"**Function:** {tool_call_data['function_name']}\n\n"
1438
665
  if tool_call_data["arguments"]:
1439
666
  try:
1440
- import json
1441
667
  args_parsed = json.loads(tool_call_data["arguments"])
1442
668
  str_output += f"**Arguments:**\n```json\n{json.dumps(args_parsed, indent=2)}\n```"
1443
669
  except:
1444
670
  str_output += f"**Arguments:** `{tool_call_data['arguments']}`"
671
+
672
+ if interrupted:
673
+ str_output += "\n\n[⚠️ Response interrupted by user]"
674
+
1445
675
 
676
+ sys.stdout.write('\033[u')
677
+ sys.stdout.write('\033[J')
678
+ sys.stdout.flush()
679
+
680
+
681
+ render_markdown(str_output)
1446
682
  print('\n')
1447
- render_markdown('\n' + str_output)
1448
683
 
1449
684
  return str_output
685
+
686
+
1450
687
  def print_and_process_stream(response, model, provider):
1451
- conversation_result = ""
1452
688
 
1453
- for chunk in response:
1454
- if provider == "ollama" and 'hf.co' in model:
1455
- chunk_content = chunk["message"]["content"]
1456
- if chunk_content:
1457
- conversation_result += chunk_content
1458
- print(chunk_content, end="")
689
+ str_output = ""
690
+ dot_count = 0
691
+ tool_call_data = {"id": None, "function_name": None, "arguments": ""}
692
+ interrupted = False
693
+
694
+ thinking_part=True
695
+ thinking_str=''
696
+ if isinstance(response, str):
697
+ render_markdown(response)
698
+ print('\n')
699
+ return response
700
+ try:
701
+ for chunk in response:
702
+
703
+ if provider == "ollama" and 'gpt-oss' not in model:
704
+
705
+ if "message" in chunk and "tool_calls" in chunk["message"]:
706
+ for tool_call in chunk["message"]["tool_calls"]:
707
+ if "id" in tool_call:
708
+ tool_call_data["id"] = tool_call["id"]
709
+ if "function" in tool_call:
710
+ if "name" in tool_call["function"]:
711
+ tool_call_data["function_name"] = tool_call["function"]["name"]
712
+ if "arguments" in tool_call["function"]:
713
+ if isinstance(tool_call["function"]["arguments"], dict):
714
+ tool_call_data["arguments"] += json.dumps(tool_call["function"]["arguments"])
715
+ else:
716
+ tool_call_data["arguments"] += tool_call["function"]["arguments"]
717
+ chunk_content = chunk["message"]["content"] if "message" in chunk and "content" in chunk["message"] else ""
718
+ reasoning_content = chunk['message'].get('thinking', '') if "message" in chunk and "thinking" in chunk['message'] else ""
719
+
720
+ if len(reasoning_content) > 0:
721
+ print(reasoning_content, end="", flush=True)
722
+ thinking_part = True
723
+ if chunk_content != "":
724
+ print(chunk_content, end="", flush=True)
725
+
726
+ else:
727
+ for c in chunk.choices:
728
+ if hasattr(c.delta, "tool_calls") and c.delta.tool_calls:
729
+ for tool_call in c.delta.tool_calls:
730
+ if tool_call.id:
731
+ tool_call_data["id"] = tool_call.id
732
+ if tool_call.function:
733
+ if hasattr(tool_call.function, "name") and tool_call.function.name:
734
+ tool_call_data["function_name"] = tool_call.function.name
735
+ if hasattr(tool_call.function, "arguments") and tool_call.function.arguments:
736
+ tool_call_data["arguments"] += tool_call.function.arguments
737
+
738
+ chunk_content = ''
739
+ reasoning_content = ''
740
+ for c in chunk.choices:
741
+ if hasattr(c.delta, "reasoning_content"):
742
+ reasoning_content += c.delta.reasoning_content
743
+
744
+
745
+ chunk_content += "".join(
746
+ c.delta.content for c in chunk.choices if c.delta.content
747
+ )
748
+ if reasoning_content is not None:
749
+ if thinking_part:
750
+ thinking_str +='<think>'
751
+ thinking_part=False
752
+ print('<think>')
753
+ print(reasoning_content, end="", flush=True)
754
+ thinking_str+=reasoning_content
755
+
756
+
757
+ if chunk_content != "":
758
+ if len(thinking_str) >0 and not thinking_part and '</think>' not in thinking_str:
1459
759
 
1460
- else:
1461
- chunk_content = "".join(
1462
- choice.delta.content
1463
- for choice in chunk.choices
1464
- if choice.delta.content is not None
1465
- )
1466
- if chunk_content:
1467
- conversation_result += chunk_content
1468
- print(chunk_content, end="")
760
+ thinking_str+='</think>'
761
+ print('</think>')
762
+ print(chunk_content, end="", flush=True)
763
+
764
+
765
+ if not chunk_content:
766
+ continue
767
+ str_output += chunk_content
768
+
769
+ except KeyboardInterrupt:
770
+ interrupted = True
771
+ print('\n⚠️ Stream interrupted by user')
772
+
773
+ if tool_call_data["id"] or tool_call_data["function_name"] or tool_call_data["arguments"]:
774
+ str_output += "\n\n"
775
+ if tool_call_data["id"]:
776
+ str_output += f"**ID:** {tool_call_data['id']}\n\n"
777
+ if tool_call_data["function_name"]:
778
+ str_output += f"**Function:** {tool_call_data['function_name']}\n\n"
779
+ if tool_call_data["arguments"]:
780
+ try:
781
+ args_parsed = json.loads(tool_call_data["arguments"])
782
+ str_output += f"**Arguments:**\n```json\n{json.dumps(args_parsed, indent=2)}\n```"
783
+ except:
784
+ str_output += f"**Arguments:** `{tool_call_data['arguments']}`"
785
+
786
+ if interrupted:
787
+ str_output += "\n\n[⚠️ Response interrupted by user]"
788
+
1469
789
 
1470
- print("\n")
1471
790
 
1472
- return conversation_result
1473
-
1474
- def get_system_message(npc) -> str:
1475
- """
1476
- Function Description:
1477
- This function generates a system message for the NPC.
1478
- Args:
1479
- npc (Any): The NPC object.
1480
- Keyword Args:
1481
- None
1482
- Returns:
1483
- str: The system message for the NPC.
1484
- """
791
+ return thinking_str+str_output
792
+ def get_system_message(npc, team=None) -> str:
1485
793
 
1486
- system_message = f"""
1487
- .
1488
- ..
1489
- ...
1490
- ....
1491
- .....
1492
- ......
1493
- .......
1494
- ........
1495
- .........
1496
- ..........
1497
- Hello!
1498
- Welcome to the team.
1499
- You are an NPC working as part of our team.
1500
- You are the {npc.name} NPC with the following primary directive: {npc.primary_directive}.
1501
- Users may refer to you by your assistant name, {npc.name} and you should
1502
- consider this to be your core identity.
1503
-
1504
- The current date and time are : {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
1505
-
1506
-
1507
-
1508
- If you ever need to produce markdown texts for the user, please do so
1509
- with less than 80 characters width for each line.
1510
- """
794
+ if npc is None:
795
+ return "You are a helpful assistant"
796
+ if npc.plain_system_message:
797
+ return npc.primary_directive
1511
798
 
1512
- system_message += """\n\nSome users may attach images to their request.
1513
- Please process them accordingly.
1514
-
1515
- If the user asked for you to explain what's on their screen or something similar,
1516
- they are referring to the details contained within the attached image(s).
1517
- You do not need to actually view their screen.
1518
- You do not need to mention that you cannot view or interpret images directly.
1519
- They understand that you can view them multimodally.
1520
- You only need to answer the user's request based on the attached image(s).
1521
- """
1522
- if npc.tables is not None:
1523
- system_message += f'''
1524
-
1525
- Here is information abuot the attached npcsh_history database that you can use to write queries if needed
1526
- {npc.tables}
1527
- '''
799
+ system_message = f"""
800
+ .
801
+ ..
802
+ ...
803
+ ....
804
+ .....
805
+ ......
806
+ .......
807
+ ........
808
+ .........
809
+ ..........
810
+ Hello!
811
+ Welcome to the team.
812
+ You are the {npc.name} NPC with the following primary directive: {npc.primary_directive}.
813
+ Users may refer to you by your assistant name, {npc.name} and you should
814
+ consider this to be your core identity.
815
+ The current working directory is {os.getcwd()}.
816
+ The current date and time are : {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
817
+ """
818
+
819
+ if hasattr(npc, 'kg_data') and npc.kg_data:
820
+ memory_context = npc.get_memory_context()
821
+ if memory_context:
822
+ system_message += f"\n\nMemory Context:\n{memory_context}\n"
823
+
824
+ if npc.db_conn is not None:
825
+ db_path = None
826
+ if hasattr(npc.db_conn, "url") and npc.db_conn.url:
827
+ db_path = npc.db_conn.url.database
828
+ elif hasattr(npc.db_conn, "database"):
829
+ db_path = npc.db_conn.database
830
+ system_message += """What follows is in
831
+ formation about the database connection. If you are asked to execute queries with tools, use this information.
832
+ If you are asked for help with debugging queries, use this information.
833
+ Do not unnecessarily reference that you possess this information unless it is
834
+ specifically relevant to the request.
835
+
836
+ DB Connection Information:
837
+ """
838
+ if db_path:
839
+ system_message += f"\nDatabase path: {db_path}\n"
840
+ if npc.tables is not None:
841
+ system_message += f"\nDatabase tables: {npc.tables}\n"
842
+
843
+ if team is not None:
844
+ team_context = team.context if hasattr(team, "context") and team.context else ""
845
+ team_preferences = team.preferences if hasattr(team, "preferences") and len(team.preferences) > 0 else ""
846
+ system_message += f"\nTeam context: {team_context}\nTeam preferences: {team_preferences}\n"
847
+
848
+ system_message += """
849
+ IMPORTANT:
850
+ Some users may attach images to their request.
851
+ Please process them accordingly. You do not need mention that you cannot "see" images. The user understands this and wants you
852
+ to help them multimodally.
853
+
854
+ If the user asked for you to explain what's on their screen or something similar,
855
+ they are referring to the details contained within the attached image(s).
856
+ You do not need to actually view their screen.
857
+ You do not need to mention that you cannot view or interpret images directly.
858
+ They understand that you can view them multimodally.
859
+ You only need to answer the user's request based on the attached image(s).
860
+ """
861
+
1528
862
  return system_message
1529
863
 
1530
864
 
1531
865
 
1532
- # Load environment variables from .env file
866
+
1533
867
  def load_env_from_execution_dir() -> None:
1534
868
  """
1535
869
  Function Description:
@@ -1542,67 +876,124 @@ def load_env_from_execution_dir() -> None:
1542
876
  None
1543
877
  """
1544
878
 
1545
- # Get the directory where the script is being executed
879
+
1546
880
  execution_dir = os.path.abspath(os.getcwd())
1547
- # print(f"Execution directory: {execution_dir}")
1548
- # Construct the path to the .env file
1549
881
  env_path = os.path.join(execution_dir, ".env")
1550
-
1551
- # Load the .env file if it exists
1552
882
  if os.path.exists(env_path):
1553
883
  load_dotenv(dotenv_path=env_path)
1554
- print(f"Loaded .env file from {execution_dir}")
884
+ logging.info(f"Loaded .env file from {execution_dir}")
1555
885
  else:
1556
- print(f"Warning: No .env file found in {execution_dir}")
886
+ logging.warning(f"Warning: No .env file found in {execution_dir}")
887
+
1557
888
 
1558
889
 
1559
890
 
1560
891
 
1561
892
  def lookup_provider(model: str) -> str:
1562
893
  """
1563
- Function Description:
1564
- This function determines the provider based on the model name.
894
+ Determine the provider based on the model name.
895
+ Checks custom providers first, then falls back to known providers.
896
+
1565
897
  Args:
1566
- model (str): The model name.
1567
- Keyword Args:
1568
- None
898
+ model: The model name
899
+
1569
900
  Returns:
1570
- str: The provider based on the model name.
901
+ The provider name or None if not found
1571
902
  """
903
+ custom_providers = load_custom_providers()
904
+
905
+ for provider_name, config in custom_providers.items():
906
+ if model.startswith(f"{provider_name}-"):
907
+ return provider_name
908
+
909
+ try:
910
+ import requests
911
+ api_key_var = config.get('api_key_var') or \
912
+ f"{provider_name.upper()}_API_KEY"
913
+ api_key = os.environ.get(api_key_var)
914
+
915
+ if api_key:
916
+ base_url = config.get('base_url', '')
917
+ headers = config.get('headers', {})
918
+ headers['Authorization'] = f'Bearer {api_key}'
919
+
920
+ models_endpoint = f"{base_url.rstrip('/')}/models"
921
+ response = requests.get(
922
+ models_endpoint,
923
+ headers=headers,
924
+ timeout=1.0
925
+ )
926
+
927
+ if response.status_code == 200:
928
+ data = response.json()
929
+ models = []
930
+
931
+ if isinstance(data, dict) and 'data' in data:
932
+ models = [m['id'] for m in data['data']]
933
+ elif isinstance(data, list):
934
+ models = [m['id'] for m in data]
935
+
936
+ if model in models:
937
+ return provider_name
938
+ except:
939
+ pass
940
+
1572
941
  if model == "deepseek-chat" or model == "deepseek-reasoner":
1573
942
  return "deepseek"
943
+
1574
944
  ollama_prefixes = [
1575
- "llama",
1576
- "deepseek",
1577
- "qwen",
1578
- "llava",
1579
- "phi",
1580
- "mistral",
1581
- "mixtral",
1582
- "dolphin",
1583
- "codellama",
1584
- "gemma",
1585
- ]
945
+ "llama", "deepseek", "qwen", "llava",
946
+ "phi", "mistral", "mixtral", "dolphin",
947
+ "codellama", "gemma",]
1586
948
  if any(model.startswith(prefix) for prefix in ollama_prefixes):
1587
949
  return "ollama"
1588
950
 
1589
- # OpenAI models
1590
951
  openai_prefixes = ["gpt-", "dall-e-", "whisper-", "o1"]
1591
952
  if any(model.startswith(prefix) for prefix in openai_prefixes):
1592
953
  return "openai"
1593
954
 
1594
- # Anthropic models
1595
955
  if model.startswith("claude"):
1596
956
  return "anthropic"
1597
957
  if model.startswith("gemini"):
1598
958
  return "gemini"
1599
959
  if "diffusion" in model:
1600
960
  return "diffusers"
961
+
1601
962
  return None
1602
963
 
1603
964
 
1604
-
1605
-
965
+ def load_custom_providers():
966
+ """
967
+ Load custom provider configurations from .npcshrc
968
+
969
+ Returns:
970
+ dict: Custom provider configurations keyed by provider name
971
+ """
972
+ custom_providers = {}
973
+ npcshrc_path = os.path.expanduser("~/.npcshrc")
974
+
975
+ if os.path.exists(npcshrc_path):
976
+ with open(npcshrc_path, "r") as f:
977
+ for line in f:
978
+ line = line.split("#")[0].strip()
979
+ if "CUSTOM_PROVIDER_" in line and "=" in line:
980
+ key, value = line.split("=", 1)
981
+ key = key.strip().replace("export ", "")
982
+ value = value.strip().strip("\"'")
983
+
984
+ try:
985
+ config = json.loads(value)
986
+ provider_name = key.replace(
987
+ "CUSTOM_PROVIDER_", ""
988
+ ).lower()
989
+ custom_providers[provider_name] = config
990
+ except json.JSONDecodeError as e:
991
+ logging.warning(
992
+ f"Failed to parse custom provider {key}: {e}"
993
+ )
994
+ continue
995
+
996
+ return custom_providers
1606
997
  load_env_from_execution_dir()
1607
998
  deepseek_api_key = os.getenv("DEEPSEEK_API_KEY", None)
1608
999
  gemini_api_key = os.getenv("GEMINI_API_KEY", None)
@@ -1610,67 +1001,3 @@ gemini_api_key = os.getenv("GEMINI_API_KEY", None)
1610
1001
  anthropic_api_key = os.getenv("ANTHROPIC_API_KEY", None)
1611
1002
  openai_api_key = os.getenv("OPENAI_API_KEY", None)
1612
1003
 
1613
- NPCSH_CHAT_MODEL = os.environ.get("NPCSH_CHAT_MODEL", "llama3.2")
1614
- # print("NPCSH_CHAT_MODEL", NPCSH_CHAT_MODEL)
1615
- NPCSH_CHAT_PROVIDER = os.environ.get("NPCSH_CHAT_PROVIDER", "ollama")
1616
- # print("NPCSH_CHAT_PROVIDER", NPCSH_CHAT_PROVIDER)
1617
- NPCSH_DB_PATH = os.path.expanduser(
1618
- os.environ.get("NPCSH_DB_PATH", "~/npcsh_history.db")
1619
- )
1620
- NPCSH_VECTOR_DB_PATH = os.path.expanduser(
1621
- os.environ.get("NPCSH_VECTOR_DB_PATH", "~/npcsh_chroma.db")
1622
- )
1623
- #DEFAULT MODES = ['CHAT', 'AGENT', 'CODE', ]
1624
-
1625
- NPCSH_DEFAULT_MODE = os.path.expanduser(os.environ.get("NPCSH_DEFAULT_MODE", "agent"))
1626
- NPCSH_VISION_MODEL = os.environ.get("NPCSH_VISION_MODEL", "llava:7b")
1627
- NPCSH_VISION_PROVIDER = os.environ.get("NPCSH_VISION_PROVIDER", "ollama")
1628
- NPCSH_IMAGE_GEN_MODEL = os.environ.get(
1629
- "NPCSH_IMAGE_GEN_MODEL", "runwayml/stable-diffusion-v1-5"
1630
- )
1631
- NPCSH_IMAGE_GEN_PROVIDER = os.environ.get("NPCSH_IMAGE_GEN_PROVIDER", "diffusers")
1632
- NPCSH_VIDEO_GEN_MODEL = os.environ.get(
1633
- "NPCSH_VIDEO_GEN_MODEL", "damo-vilab/text-to-video-ms-1.7b"
1634
- )
1635
- NPCSH_VIDEO_GEN_PROVIDER = os.environ.get("NPCSH_VIDEO_GEN_PROVIDER", "diffusers")
1636
-
1637
- NPCSH_EMBEDDING_MODEL = os.environ.get("NPCSH_EMBEDDING_MODEL", "nomic-embed-text")
1638
- NPCSH_EMBEDDING_PROVIDER = os.environ.get("NPCSH_EMBEDDING_PROVIDER", "ollama")
1639
- NPCSH_REASONING_MODEL = os.environ.get("NPCSH_REASONING_MODEL", "deepseek-r1")
1640
- NPCSH_REASONING_PROVIDER = os.environ.get("NPCSH_REASONING_PROVIDER", "ollama")
1641
- NPCSH_STREAM_OUTPUT = eval(os.environ.get("NPCSH_STREAM_OUTPUT", "0")) == 1
1642
- NPCSH_API_URL = os.environ.get("NPCSH_API_URL", None)
1643
- NPCSH_SEARCH_PROVIDER = os.environ.get("NPCSH_SEARCH_PROVIDER", "duckduckgo")
1644
-
1645
- READLINE_HISTORY_FILE = os.path.expanduser("~/.npcsh_history")
1646
- def setup_readline() -> str:
1647
- if readline is None:
1648
- return None
1649
- try:
1650
- readline.read_history_file(READLINE_HISTORY_FILE)
1651
- readline.set_history_length(1000)
1652
- readline.parse_and_bind("set enable-bracketed-paste on")
1653
- readline.parse_and_bind(r'"\e[A": history-search-backward')
1654
- readline.parse_and_bind(r'"\e[B": history-search-forward')
1655
- readline.parse_and_bind(r'"\C-r": reverse-search-history')
1656
- readline.parse_and_bind(r'\C-e: end-of-line')
1657
- readline.parse_and_bind(r'\C-a: beginning-of-line')
1658
- if sys.platform == "darwin":
1659
- readline.parse_and_bind("bind ^I rl_complete")
1660
- else:
1661
- readline.parse_and_bind("tab: complete")
1662
- return READLINE_HISTORY_FILE
1663
- except FileNotFoundError:
1664
- pass
1665
- except OSError as e:
1666
- print(f"Warning: Could not read readline history file {READLINE_HISTORY_FILE}: {e}")
1667
-
1668
- def save_readline_history():
1669
- if readline is None:
1670
- return
1671
- try:
1672
- readline.write_history_file(READLINE_HISTORY_FILE)
1673
- except OSError as e:
1674
- print(f"Warning: Could not write readline history file {READLINE_HISTORY_FILE}: {e}")
1675
-
1676
-