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.
- npcpy/__init__.py +0 -7
- npcpy/data/audio.py +16 -99
- npcpy/data/image.py +43 -42
- npcpy/data/load.py +83 -124
- npcpy/data/text.py +28 -28
- npcpy/data/video.py +8 -32
- npcpy/data/web.py +51 -23
- npcpy/ft/diff.py +110 -0
- npcpy/ft/ge.py +115 -0
- npcpy/ft/memory_trainer.py +171 -0
- npcpy/ft/model_ensembler.py +357 -0
- npcpy/ft/rl.py +360 -0
- npcpy/ft/sft.py +248 -0
- npcpy/ft/usft.py +128 -0
- npcpy/gen/audio_gen.py +24 -0
- npcpy/gen/embeddings.py +13 -13
- npcpy/gen/image_gen.py +262 -117
- npcpy/gen/response.py +615 -415
- npcpy/gen/video_gen.py +53 -7
- npcpy/llm_funcs.py +1869 -437
- npcpy/main.py +1 -1
- npcpy/memory/command_history.py +844 -510
- npcpy/memory/kg_vis.py +833 -0
- npcpy/memory/knowledge_graph.py +892 -1845
- npcpy/memory/memory_processor.py +81 -0
- npcpy/memory/search.py +188 -90
- npcpy/mix/debate.py +192 -3
- npcpy/npc_compiler.py +1672 -801
- npcpy/npc_sysenv.py +593 -1266
- npcpy/serve.py +3120 -0
- npcpy/sql/ai_function_tools.py +257 -0
- npcpy/sql/database_ai_adapters.py +186 -0
- npcpy/sql/database_ai_functions.py +163 -0
- npcpy/sql/model_runner.py +19 -19
- npcpy/sql/npcsql.py +706 -507
- npcpy/sql/sql_model_compiler.py +156 -0
- npcpy/tools.py +183 -0
- npcpy/work/plan.py +13 -279
- npcpy/work/trigger.py +3 -3
- npcpy-1.2.32.dist-info/METADATA +803 -0
- npcpy-1.2.32.dist-info/RECORD +54 -0
- npcpy/data/dataframes.py +0 -171
- npcpy/memory/deep_research.py +0 -125
- npcpy/memory/sleep.py +0 -557
- npcpy/modes/_state.py +0 -78
- npcpy/modes/alicanto.py +0 -1075
- npcpy/modes/guac.py +0 -785
- npcpy/modes/mcp_npcsh.py +0 -822
- npcpy/modes/npc.py +0 -213
- npcpy/modes/npcsh.py +0 -1158
- npcpy/modes/plonk.py +0 -409
- npcpy/modes/pti.py +0 -234
- npcpy/modes/serve.py +0 -1637
- npcpy/modes/spool.py +0 -312
- npcpy/modes/wander.py +0 -549
- npcpy/modes/yap.py +0 -572
- npcpy/npc_team/alicanto.npc +0 -2
- npcpy/npc_team/alicanto.png +0 -0
- npcpy/npc_team/assembly_lines/test_pipeline.py +0 -181
- npcpy/npc_team/corca.npc +0 -13
- npcpy/npc_team/foreman.npc +0 -7
- npcpy/npc_team/frederic.npc +0 -6
- npcpy/npc_team/frederic4.png +0 -0
- npcpy/npc_team/guac.png +0 -0
- npcpy/npc_team/jinxs/automator.jinx +0 -18
- npcpy/npc_team/jinxs/bash_executer.jinx +0 -31
- npcpy/npc_team/jinxs/calculator.jinx +0 -11
- npcpy/npc_team/jinxs/edit_file.jinx +0 -96
- npcpy/npc_team/jinxs/file_chat.jinx +0 -14
- npcpy/npc_team/jinxs/gui_controller.jinx +0 -28
- npcpy/npc_team/jinxs/image_generation.jinx +0 -29
- npcpy/npc_team/jinxs/internet_search.jinx +0 -30
- npcpy/npc_team/jinxs/local_search.jinx +0 -152
- npcpy/npc_team/jinxs/npcsh_executor.jinx +0 -31
- npcpy/npc_team/jinxs/python_executor.jinx +0 -8
- npcpy/npc_team/jinxs/screen_cap.jinx +0 -25
- npcpy/npc_team/jinxs/sql_executor.jinx +0 -33
- npcpy/npc_team/kadiefa.npc +0 -3
- npcpy/npc_team/kadiefa.png +0 -0
- npcpy/npc_team/npcsh.ctx +0 -9
- npcpy/npc_team/npcsh_sibiji.png +0 -0
- npcpy/npc_team/plonk.npc +0 -2
- npcpy/npc_team/plonk.png +0 -0
- npcpy/npc_team/plonkjr.npc +0 -2
- npcpy/npc_team/plonkjr.png +0 -0
- npcpy/npc_team/sibiji.npc +0 -5
- npcpy/npc_team/sibiji.png +0 -0
- npcpy/npc_team/spool.png +0 -0
- npcpy/npc_team/templates/analytics/celona.npc +0 -0
- npcpy/npc_team/templates/hr_support/raone.npc +0 -0
- npcpy/npc_team/templates/humanities/eriane.npc +0 -4
- npcpy/npc_team/templates/it_support/lineru.npc +0 -0
- npcpy/npc_team/templates/marketing/slean.npc +0 -4
- npcpy/npc_team/templates/philosophy/maurawa.npc +0 -0
- npcpy/npc_team/templates/sales/turnic.npc +0 -4
- npcpy/npc_team/templates/software/welxor.npc +0 -0
- npcpy/npc_team/yap.png +0 -0
- npcpy/routes.py +0 -958
- npcpy/work/mcp_helpers.py +0 -357
- npcpy/work/mcp_server.py +0 -194
- npcpy-1.0.26.data/data/npcpy/npc_team/alicanto.npc +0 -2
- npcpy-1.0.26.data/data/npcpy/npc_team/alicanto.png +0 -0
- npcpy-1.0.26.data/data/npcpy/npc_team/automator.jinx +0 -18
- npcpy-1.0.26.data/data/npcpy/npc_team/bash_executer.jinx +0 -31
- npcpy-1.0.26.data/data/npcpy/npc_team/calculator.jinx +0 -11
- npcpy-1.0.26.data/data/npcpy/npc_team/celona.npc +0 -0
- npcpy-1.0.26.data/data/npcpy/npc_team/corca.npc +0 -13
- npcpy-1.0.26.data/data/npcpy/npc_team/edit_file.jinx +0 -96
- npcpy-1.0.26.data/data/npcpy/npc_team/eriane.npc +0 -4
- npcpy-1.0.26.data/data/npcpy/npc_team/file_chat.jinx +0 -14
- npcpy-1.0.26.data/data/npcpy/npc_team/foreman.npc +0 -7
- npcpy-1.0.26.data/data/npcpy/npc_team/frederic.npc +0 -6
- npcpy-1.0.26.data/data/npcpy/npc_team/frederic4.png +0 -0
- npcpy-1.0.26.data/data/npcpy/npc_team/guac.png +0 -0
- npcpy-1.0.26.data/data/npcpy/npc_team/gui_controller.jinx +0 -28
- npcpy-1.0.26.data/data/npcpy/npc_team/image_generation.jinx +0 -29
- npcpy-1.0.26.data/data/npcpy/npc_team/internet_search.jinx +0 -30
- npcpy-1.0.26.data/data/npcpy/npc_team/kadiefa.npc +0 -3
- npcpy-1.0.26.data/data/npcpy/npc_team/kadiefa.png +0 -0
- npcpy-1.0.26.data/data/npcpy/npc_team/lineru.npc +0 -0
- npcpy-1.0.26.data/data/npcpy/npc_team/local_search.jinx +0 -152
- npcpy-1.0.26.data/data/npcpy/npc_team/maurawa.npc +0 -0
- npcpy-1.0.26.data/data/npcpy/npc_team/npcsh.ctx +0 -9
- npcpy-1.0.26.data/data/npcpy/npc_team/npcsh_executor.jinx +0 -31
- npcpy-1.0.26.data/data/npcpy/npc_team/npcsh_sibiji.png +0 -0
- npcpy-1.0.26.data/data/npcpy/npc_team/plonk.npc +0 -2
- npcpy-1.0.26.data/data/npcpy/npc_team/plonk.png +0 -0
- npcpy-1.0.26.data/data/npcpy/npc_team/plonkjr.npc +0 -2
- npcpy-1.0.26.data/data/npcpy/npc_team/plonkjr.png +0 -0
- npcpy-1.0.26.data/data/npcpy/npc_team/python_executor.jinx +0 -8
- npcpy-1.0.26.data/data/npcpy/npc_team/raone.npc +0 -0
- npcpy-1.0.26.data/data/npcpy/npc_team/screen_cap.jinx +0 -25
- npcpy-1.0.26.data/data/npcpy/npc_team/sibiji.npc +0 -5
- npcpy-1.0.26.data/data/npcpy/npc_team/sibiji.png +0 -0
- npcpy-1.0.26.data/data/npcpy/npc_team/slean.npc +0 -4
- npcpy-1.0.26.data/data/npcpy/npc_team/spool.png +0 -0
- npcpy-1.0.26.data/data/npcpy/npc_team/sql_executor.jinx +0 -33
- npcpy-1.0.26.data/data/npcpy/npc_team/test_pipeline.py +0 -181
- npcpy-1.0.26.data/data/npcpy/npc_team/turnic.npc +0 -4
- npcpy-1.0.26.data/data/npcpy/npc_team/welxor.npc +0 -0
- npcpy-1.0.26.data/data/npcpy/npc_team/yap.png +0 -0
- npcpy-1.0.26.dist-info/METADATA +0 -827
- npcpy-1.0.26.dist-info/RECORD +0 -139
- npcpy-1.0.26.dist-info/entry_points.txt +0 -11
- /npcpy/{modes → ft}/__init__.py +0 -0
- {npcpy-1.0.26.dist-info → npcpy-1.2.32.dist-info}/WHEEL +0 -0
- {npcpy-1.0.26.dist-info → npcpy-1.2.32.dist-info}/licenses/LICENSE +0 -0
- {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
|
|
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
|
|
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
|
-
|
|
33
|
+
|
|
67
34
|
try:
|
|
68
35
|
import readline
|
|
69
36
|
except ImportError:
|
|
70
37
|
readline = None
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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("```"):
|
|
348
|
+
if line.startswith("```"):
|
|
382
349
|
if inside_code_block:
|
|
383
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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]
|
|
1344
|
-
|
|
544
|
+
model_name = matches[0]
|
|
545
|
+
|
|
1345
546
|
provider = lookup_provider(model_name)
|
|
1346
547
|
if provider:
|
|
1347
|
-
|
|
548
|
+
|
|
1348
549
|
cleaned_command = command.replace(
|
|
1349
550
|
f"@{model_match.group(1)}", ""
|
|
1350
551
|
).strip()
|
|
1351
|
-
|
|
552
|
+
|
|
1352
553
|
return model_name, provider, cleaned_command
|
|
1353
554
|
else:
|
|
1354
|
-
return None, None, command
|
|
555
|
+
return None, None, command
|
|
1355
556
|
else:
|
|
1356
|
-
return None, None, command
|
|
557
|
+
return None, None, command
|
|
1357
558
|
else:
|
|
1358
|
-
return None, None, command
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
580
|
+
dot_count = 0
|
|
1377
581
|
tool_call_data = {"id": None, "function_name": None, "arguments": ""}
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
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
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
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
|
-
|
|
1429
|
-
|
|
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
|
|
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
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
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
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
884
|
+
logging.info(f"Loaded .env file from {execution_dir}")
|
|
1555
885
|
else:
|
|
1556
|
-
|
|
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
|
-
|
|
1564
|
-
|
|
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
|
|
1567
|
-
|
|
1568
|
-
None
|
|
898
|
+
model: The model name
|
|
899
|
+
|
|
1569
900
|
Returns:
|
|
1570
|
-
|
|
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
|
-
"
|
|
1577
|
-
"
|
|
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
|
-
|