npcpy 1.1.28__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/data/audio.py +16 -38
- npcpy/data/image.py +29 -29
- npcpy/data/load.py +4 -3
- npcpy/data/text.py +28 -28
- npcpy/data/video.py +6 -6
- npcpy/data/web.py +49 -21
- npcpy/ft/__init__.py +0 -0
- 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 +37 -15
- npcpy/gen/response.py +287 -111
- npcpy/gen/video_gen.py +10 -9
- npcpy/llm_funcs.py +447 -79
- npcpy/memory/command_history.py +201 -48
- npcpy/memory/kg_vis.py +74 -74
- npcpy/memory/knowledge_graph.py +482 -115
- npcpy/memory/memory_processor.py +81 -0
- npcpy/memory/search.py +70 -70
- npcpy/mix/debate.py +192 -3
- npcpy/npc_compiler.py +1541 -879
- npcpy/npc_sysenv.py +250 -78
- npcpy/serve.py +1036 -321
- 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 +20 -20
- npcpy/work/plan.py +8 -8
- npcpy/work/trigger.py +3 -3
- {npcpy-1.1.28.dist-info → npcpy-1.2.32.dist-info}/METADATA +169 -9
- npcpy-1.2.32.dist-info/RECORD +54 -0
- npcpy-1.1.28.dist-info/RECORD +0 -40
- {npcpy-1.1.28.dist-info → npcpy-1.2.32.dist-info}/WHEEL +0 -0
- {npcpy-1.1.28.dist-info → npcpy-1.2.32.dist-info}/licenses/LICENSE +0 -0
- {npcpy-1.1.28.dist-info → npcpy-1.2.32.dist-info}/top_level.txt +0 -0
npcpy/npc_sysenv.py
CHANGED
|
@@ -4,16 +4,18 @@ import logging
|
|
|
4
4
|
import re
|
|
5
5
|
import os
|
|
6
6
|
import socket
|
|
7
|
-
import concurrent.futures
|
|
7
|
+
import concurrent.futures
|
|
8
8
|
import platform
|
|
9
9
|
import sqlite3
|
|
10
10
|
import sys
|
|
11
11
|
from typing import Dict, List
|
|
12
12
|
import textwrap
|
|
13
13
|
import json
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
import requests
|
|
14
17
|
ON_WINDOWS = platform.system() == "Windows"
|
|
15
18
|
|
|
16
|
-
# Try/except for termios, tty, pty, select, signal
|
|
17
19
|
try:
|
|
18
20
|
if not ON_WINDOWS:
|
|
19
21
|
import termios
|
|
@@ -28,14 +30,14 @@ except ImportError:
|
|
|
28
30
|
select = None
|
|
29
31
|
signal = None
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
|
|
32
34
|
try:
|
|
33
35
|
import readline
|
|
34
36
|
except ImportError:
|
|
35
37
|
readline = None
|
|
36
38
|
logging.warning('no readline support, some features may not work as desired.')
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
|
|
39
41
|
try:
|
|
40
42
|
from rich.console import Console
|
|
41
43
|
from rich.markdown import Markdown
|
|
@@ -48,7 +50,7 @@ except ImportError:
|
|
|
48
50
|
import warnings
|
|
49
51
|
import time
|
|
50
52
|
|
|
51
|
-
|
|
53
|
+
|
|
52
54
|
running = True
|
|
53
55
|
is_recording = False
|
|
54
56
|
recording_data = []
|
|
@@ -61,7 +63,7 @@ warnings.filterwarnings("ignore", module="torch.serialization")
|
|
|
61
63
|
os.environ["PYTHONWARNINGS"] = "ignore"
|
|
62
64
|
os.environ["SDL_AUDIODRIVER"] = "dummy"
|
|
63
65
|
|
|
64
|
-
def check_internet_connection(timeout=
|
|
66
|
+
def check_internet_connection(timeout=5):
|
|
65
67
|
"""
|
|
66
68
|
Checks for internet connectivity by trying to connect to a well-known host.
|
|
67
69
|
"""
|
|
@@ -85,38 +87,143 @@ def get_locally_available_models(project_directory, airplane_mode=False):
|
|
|
85
87
|
key, value = line.split("=", 1)
|
|
86
88
|
env_vars[key.strip()] = value.strip().strip("\"'")
|
|
87
89
|
|
|
88
|
-
# --- Internet check here ---
|
|
89
90
|
internet_available = check_internet_connection()
|
|
90
91
|
if not internet_available:
|
|
91
|
-
logging.info(
|
|
92
|
-
|
|
92
|
+
logging.info(
|
|
93
|
+
"No internet connection detected. "
|
|
94
|
+
"External API calls will be skipped."
|
|
95
|
+
)
|
|
93
96
|
airplane_mode = True
|
|
94
97
|
else:
|
|
95
|
-
logging.info(
|
|
96
|
-
|
|
97
|
-
|
|
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', {})
|
|
117
|
+
|
|
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
|
|
128
|
+
)
|
|
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
|
|
98
162
|
if not airplane_mode:
|
|
99
163
|
timeout_seconds = 3.5
|
|
100
|
-
|
|
164
|
+
|
|
165
|
+
|
|
101
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
|
+
|
|
102
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
|
+
|
|
209
|
+
|
|
103
210
|
if "ANTHROPIC_API_KEY" in env_vars or os.environ.get("ANTHROPIC_API_KEY"):
|
|
104
211
|
try:
|
|
105
212
|
import anthropic
|
|
106
213
|
|
|
107
214
|
def fetch_anthropic_models():
|
|
108
215
|
client = anthropic.Anthropic(api_key=env_vars.get("ANTHROPIC_API_KEY") or os.environ.get("ANTHROPIC_API_KEY"))
|
|
109
|
-
|
|
110
|
-
|
|
216
|
+
|
|
217
|
+
|
|
111
218
|
return client.models.list()
|
|
112
219
|
|
|
113
220
|
future = executor.submit(fetch_anthropic_models)
|
|
114
|
-
models = future.result(timeout=timeout_seconds)
|
|
221
|
+
models = future.result(timeout=timeout_seconds)
|
|
115
222
|
|
|
116
223
|
for model in models.data:
|
|
117
224
|
available_models[model.id] = 'anthropic'
|
|
118
225
|
|
|
119
|
-
except (ImportError,
|
|
226
|
+
except (ImportError, concurrent.futures.TimeoutError, Exception) as e:
|
|
120
227
|
logging.info(f"Anthropic models not indexed or timed out: {e}")
|
|
121
228
|
|
|
122
229
|
if "OPENAI_API_KEY" in env_vars or os.environ.get("OPENAI_API_KEY"):
|
|
@@ -128,7 +235,7 @@ def get_locally_available_models(project_directory, airplane_mode=False):
|
|
|
128
235
|
return openai.models.list()
|
|
129
236
|
|
|
130
237
|
future = executor.submit(fetch_openai_models)
|
|
131
|
-
models = future.result(timeout=timeout_seconds)
|
|
238
|
+
models = future.result(timeout=timeout_seconds)
|
|
132
239
|
|
|
133
240
|
for model in models.data:
|
|
134
241
|
if (
|
|
@@ -151,7 +258,7 @@ def get_locally_available_models(project_directory, airplane_mode=False):
|
|
|
151
258
|
def fetch_gemini_models():
|
|
152
259
|
client = genai.Client(api_key=env_vars.get("GEMINI_API_KEY") or os.environ.get("GEMINI_API_KEY"))
|
|
153
260
|
found_models = []
|
|
154
|
-
|
|
261
|
+
|
|
155
262
|
target_models = [
|
|
156
263
|
'gemini-2.5-pro',
|
|
157
264
|
'gemini-2.5-flash',
|
|
@@ -165,15 +272,15 @@ def get_locally_available_models(project_directory, airplane_mode=False):
|
|
|
165
272
|
for action in m.supported_actions:
|
|
166
273
|
if action == "generateContent":
|
|
167
274
|
if 'models/' in m.name:
|
|
168
|
-
model_name_part = m.name.split('/')[1]
|
|
169
|
-
|
|
275
|
+
model_name_part = m.name.split('/')[1]
|
|
276
|
+
|
|
170
277
|
if any(model in model_name_part for model in target_models):
|
|
171
278
|
found_models.append(model_name_part)
|
|
172
279
|
return set(found_models)
|
|
173
280
|
future = executor.submit(fetch_gemini_models)
|
|
174
|
-
models = future.result(timeout=timeout_seconds)
|
|
281
|
+
models = future.result(timeout=timeout_seconds)
|
|
175
282
|
|
|
176
|
-
for model in models:
|
|
283
|
+
for model in models:
|
|
177
284
|
if "gemini" in model:
|
|
178
285
|
available_models[model] = "gemini"
|
|
179
286
|
except (ImportError, concurrent.futures.TimeoutError, Exception) as e:
|
|
@@ -184,13 +291,13 @@ def get_locally_available_models(project_directory, airplane_mode=False):
|
|
|
184
291
|
available_models['deepseek-reasoner'] = 'deepseek'
|
|
185
292
|
try:
|
|
186
293
|
import ollama
|
|
187
|
-
timeout_seconds = 0.5
|
|
294
|
+
timeout_seconds = 0.5
|
|
188
295
|
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as ollama_executor:
|
|
189
296
|
def fetch_ollama_models():
|
|
190
297
|
return ollama.list()
|
|
191
298
|
|
|
192
299
|
future = ollama_executor.submit(fetch_ollama_models)
|
|
193
|
-
models = future.result(timeout=timeout_seconds)
|
|
300
|
+
models = future.result(timeout=timeout_seconds)
|
|
194
301
|
|
|
195
302
|
for model in models.models:
|
|
196
303
|
if "embed" not in model.model:
|
|
@@ -238,9 +345,9 @@ def preprocess_markdown(md_text):
|
|
|
238
345
|
current_code_block = []
|
|
239
346
|
|
|
240
347
|
for line in lines:
|
|
241
|
-
if line.startswith("```"):
|
|
348
|
+
if line.startswith("```"):
|
|
242
349
|
if inside_code_block:
|
|
243
|
-
|
|
350
|
+
|
|
244
351
|
processed_lines.append("```")
|
|
245
352
|
processed_lines.extend(
|
|
246
353
|
textwrap.dedent("\n".join(current_code_block)).split("\n")
|
|
@@ -285,7 +392,7 @@ def render_markdown(text: str) -> None:
|
|
|
285
392
|
for line in lines:
|
|
286
393
|
if line.startswith("```"):
|
|
287
394
|
if inside_code_block:
|
|
288
|
-
|
|
395
|
+
|
|
289
396
|
code = "\n".join(code_lines)
|
|
290
397
|
if code.strip():
|
|
291
398
|
syntax = Syntax(
|
|
@@ -294,13 +401,13 @@ def render_markdown(text: str) -> None:
|
|
|
294
401
|
console.print(syntax)
|
|
295
402
|
code_lines = []
|
|
296
403
|
else:
|
|
297
|
-
|
|
404
|
+
|
|
298
405
|
lang = line[3:].strip() or None
|
|
299
406
|
inside_code_block = not inside_code_block
|
|
300
407
|
elif inside_code_block:
|
|
301
408
|
code_lines.append(line)
|
|
302
409
|
else:
|
|
303
|
-
|
|
410
|
+
|
|
304
411
|
console.print(Markdown(line))
|
|
305
412
|
|
|
306
413
|
def get_directory_npcs(directory: str = None) -> List[str]:
|
|
@@ -371,7 +478,7 @@ def init_db_tables(db_path="~/npcsh_history.db"):
|
|
|
371
478
|
"""Initialize necessary database tables"""
|
|
372
479
|
db_path = os.path.expanduser(db_path)
|
|
373
480
|
with sqlite3.connect(db_path) as conn:
|
|
374
|
-
|
|
481
|
+
|
|
375
482
|
conn.execute("""
|
|
376
483
|
CREATE TABLE IF NOT EXISTS npc_log (
|
|
377
484
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -383,7 +490,7 @@ def init_db_tables(db_path="~/npcsh_history.db"):
|
|
|
383
490
|
)
|
|
384
491
|
""")
|
|
385
492
|
|
|
386
|
-
|
|
493
|
+
|
|
387
494
|
conn.execute("""
|
|
388
495
|
CREATE TABLE IF NOT EXISTS pipeline_runs (
|
|
389
496
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -394,7 +501,7 @@ def init_db_tables(db_path="~/npcsh_history.db"):
|
|
|
394
501
|
)
|
|
395
502
|
""")
|
|
396
503
|
|
|
397
|
-
|
|
504
|
+
|
|
398
505
|
conn.execute("""
|
|
399
506
|
CREATE TABLE IF NOT EXISTS compiled_npcs (
|
|
400
507
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -430,26 +537,26 @@ def get_model_and_provider(command: str, available_models: list) -> tuple:
|
|
|
430
537
|
model_match = re.search(r"@(\S+)", command)
|
|
431
538
|
if model_match:
|
|
432
539
|
model_name = model_match.group(1)
|
|
433
|
-
|
|
540
|
+
|
|
434
541
|
matches = [m for m in available_models if m.startswith(model_name)]
|
|
435
542
|
if matches:
|
|
436
543
|
if len(matches) == 1:
|
|
437
|
-
model_name = matches[0]
|
|
438
|
-
|
|
544
|
+
model_name = matches[0]
|
|
545
|
+
|
|
439
546
|
provider = lookup_provider(model_name)
|
|
440
547
|
if provider:
|
|
441
|
-
|
|
548
|
+
|
|
442
549
|
cleaned_command = command.replace(
|
|
443
550
|
f"@{model_match.group(1)}", ""
|
|
444
551
|
).strip()
|
|
445
|
-
|
|
552
|
+
|
|
446
553
|
return model_name, provider, cleaned_command
|
|
447
554
|
else:
|
|
448
|
-
return None, None, command
|
|
555
|
+
return None, None, command
|
|
449
556
|
else:
|
|
450
|
-
return None, None, command
|
|
557
|
+
return None, None, command
|
|
451
558
|
else:
|
|
452
|
-
return None, None, command
|
|
559
|
+
return None, None, command
|
|
453
560
|
|
|
454
561
|
def render_code_block(code: str, language: str = None) -> None:
|
|
455
562
|
"""Render a code block with syntax highlighting using rich, left-justified with no line numbers"""
|
|
@@ -458,7 +565,7 @@ def render_code_block(code: str, language: str = None) -> None:
|
|
|
458
565
|
|
|
459
566
|
console = Console(highlight=True)
|
|
460
567
|
code = code.strip()
|
|
461
|
-
|
|
568
|
+
|
|
462
569
|
if code.split("\n", 1)[0].lower() in ["python", "bash", "javascript"]:
|
|
463
570
|
code = code.split("\n", 1)[1]
|
|
464
571
|
syntax = Syntax(
|
|
@@ -479,8 +586,8 @@ def print_and_process_stream_with_markdown(response, model, provider, show=False
|
|
|
479
586
|
print('\n')
|
|
480
587
|
return response
|
|
481
588
|
|
|
482
|
-
|
|
483
|
-
sys.stdout.write('\033[s')
|
|
589
|
+
|
|
590
|
+
sys.stdout.write('\033[s')
|
|
484
591
|
sys.stdout.flush()
|
|
485
592
|
|
|
486
593
|
try:
|
|
@@ -528,10 +635,7 @@ def print_and_process_stream_with_markdown(response, model, provider, show=False
|
|
|
528
635
|
for c in chunk.choices:
|
|
529
636
|
if hasattr(c.delta, "reasoning_content"):
|
|
530
637
|
reasoning_content += c.delta.reasoning_content
|
|
531
|
-
|
|
532
|
-
if len(reasoning_content) > 0:
|
|
533
|
-
chunk_content = reasoning_content
|
|
534
|
-
|
|
638
|
+
|
|
535
639
|
chunk_content += "".join(
|
|
536
640
|
c.delta.content for c in chunk.choices if c.delta.content
|
|
537
641
|
)
|
|
@@ -553,7 +657,7 @@ def print_and_process_stream_with_markdown(response, model, provider, show=False
|
|
|
553
657
|
print('\n⚠️ Stream interrupted by user')
|
|
554
658
|
|
|
555
659
|
if tool_call_data["id"] or tool_call_data["function_name"] or tool_call_data["arguments"]:
|
|
556
|
-
str_output += "\n\n
|
|
660
|
+
str_output += "\n\n"
|
|
557
661
|
if tool_call_data["id"]:
|
|
558
662
|
str_output += f"**ID:** {tool_call_data['id']}\n\n"
|
|
559
663
|
if tool_call_data["function_name"]:
|
|
@@ -568,12 +672,12 @@ def print_and_process_stream_with_markdown(response, model, provider, show=False
|
|
|
568
672
|
if interrupted:
|
|
569
673
|
str_output += "\n\n[⚠️ Response interrupted by user]"
|
|
570
674
|
|
|
571
|
-
|
|
572
|
-
sys.stdout.write('\033[u')
|
|
573
|
-
sys.stdout.write('\033[J')
|
|
675
|
+
|
|
676
|
+
sys.stdout.write('\033[u')
|
|
677
|
+
sys.stdout.write('\033[J')
|
|
574
678
|
sys.stdout.flush()
|
|
575
679
|
|
|
576
|
-
|
|
680
|
+
|
|
577
681
|
render_markdown(str_output)
|
|
578
682
|
print('\n')
|
|
579
683
|
|
|
@@ -648,7 +752,7 @@ def print_and_process_stream(response, model, provider):
|
|
|
648
752
|
print('<think>')
|
|
649
753
|
print(reasoning_content, end="", flush=True)
|
|
650
754
|
thinking_str+=reasoning_content
|
|
651
|
-
|
|
755
|
+
|
|
652
756
|
|
|
653
757
|
if chunk_content != "":
|
|
654
758
|
if len(thinking_str) >0 and not thinking_part and '</think>' not in thinking_str:
|
|
@@ -667,7 +771,7 @@ def print_and_process_stream(response, model, provider):
|
|
|
667
771
|
print('\n⚠️ Stream interrupted by user')
|
|
668
772
|
|
|
669
773
|
if tool_call_data["id"] or tool_call_data["function_name"] or tool_call_data["arguments"]:
|
|
670
|
-
str_output += "\n\n
|
|
774
|
+
str_output += "\n\n"
|
|
671
775
|
if tool_call_data["id"]:
|
|
672
776
|
str_output += f"**ID:** {tool_call_data['id']}\n\n"
|
|
673
777
|
if tool_call_data["function_name"]:
|
|
@@ -686,8 +790,6 @@ def print_and_process_stream(response, model, provider):
|
|
|
686
790
|
|
|
687
791
|
return thinking_str+str_output
|
|
688
792
|
def get_system_message(npc, team=None) -> str:
|
|
689
|
-
import os
|
|
690
|
-
from datetime import datetime
|
|
691
793
|
|
|
692
794
|
if npc is None:
|
|
693
795
|
return "You are a helpful assistant"
|
|
@@ -714,6 +816,11 @@ The current working directory is {os.getcwd()}.
|
|
|
714
816
|
The current date and time are : {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
|
|
715
817
|
"""
|
|
716
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
|
+
|
|
717
824
|
if npc.db_conn is not None:
|
|
718
825
|
db_path = None
|
|
719
826
|
if hasattr(npc.db_conn, "url") and npc.db_conn.url:
|
|
@@ -738,7 +845,6 @@ The current date and time are : {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
|
|
|
738
845
|
team_preferences = team.preferences if hasattr(team, "preferences") and len(team.preferences) > 0 else ""
|
|
739
846
|
system_message += f"\nTeam context: {team_context}\nTeam preferences: {team_preferences}\n"
|
|
740
847
|
|
|
741
|
-
|
|
742
848
|
system_message += """
|
|
743
849
|
IMPORTANT:
|
|
744
850
|
Some users may attach images to their request.
|
|
@@ -753,11 +859,11 @@ They understand that you can view them multimodally.
|
|
|
753
859
|
You only need to answer the user's request based on the attached image(s).
|
|
754
860
|
"""
|
|
755
861
|
|
|
756
|
-
|
|
757
862
|
return system_message
|
|
758
863
|
|
|
759
864
|
|
|
760
865
|
|
|
866
|
+
|
|
761
867
|
def load_env_from_execution_dir() -> None:
|
|
762
868
|
"""
|
|
763
869
|
Function Description:
|
|
@@ -782,50 +888,116 @@ def load_env_from_execution_dir() -> None:
|
|
|
782
888
|
|
|
783
889
|
|
|
784
890
|
|
|
891
|
+
|
|
785
892
|
def lookup_provider(model: str) -> str:
|
|
786
893
|
"""
|
|
787
|
-
|
|
788
|
-
|
|
894
|
+
Determine the provider based on the model name.
|
|
895
|
+
Checks custom providers first, then falls back to known providers.
|
|
896
|
+
|
|
789
897
|
Args:
|
|
790
|
-
model
|
|
791
|
-
|
|
792
|
-
None
|
|
898
|
+
model: The model name
|
|
899
|
+
|
|
793
900
|
Returns:
|
|
794
|
-
|
|
901
|
+
The provider name or None if not found
|
|
795
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
|
+
|
|
796
941
|
if model == "deepseek-chat" or model == "deepseek-reasoner":
|
|
797
942
|
return "deepseek"
|
|
943
|
+
|
|
798
944
|
ollama_prefixes = [
|
|
799
|
-
"llama",
|
|
800
|
-
"
|
|
801
|
-
"
|
|
802
|
-
"llava",
|
|
803
|
-
"phi",
|
|
804
|
-
"mistral",
|
|
805
|
-
"mixtral",
|
|
806
|
-
"dolphin",
|
|
807
|
-
"codellama",
|
|
808
|
-
"gemma",
|
|
809
|
-
]
|
|
945
|
+
"llama", "deepseek", "qwen", "llava",
|
|
946
|
+
"phi", "mistral", "mixtral", "dolphin",
|
|
947
|
+
"codellama", "gemma",]
|
|
810
948
|
if any(model.startswith(prefix) for prefix in ollama_prefixes):
|
|
811
949
|
return "ollama"
|
|
812
950
|
|
|
813
|
-
# OpenAI models
|
|
814
951
|
openai_prefixes = ["gpt-", "dall-e-", "whisper-", "o1"]
|
|
815
952
|
if any(model.startswith(prefix) for prefix in openai_prefixes):
|
|
816
953
|
return "openai"
|
|
817
954
|
|
|
818
|
-
# Anthropic models
|
|
819
955
|
if model.startswith("claude"):
|
|
820
956
|
return "anthropic"
|
|
821
957
|
if model.startswith("gemini"):
|
|
822
958
|
return "gemini"
|
|
823
959
|
if "diffusion" in model:
|
|
824
960
|
return "diffusers"
|
|
961
|
+
|
|
825
962
|
return None
|
|
963
|
+
|
|
964
|
+
|
|
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
|
|
826
997
|
load_env_from_execution_dir()
|
|
827
998
|
deepseek_api_key = os.getenv("DEEPSEEK_API_KEY", None)
|
|
828
999
|
gemini_api_key = os.getenv("GEMINI_API_KEY", None)
|
|
829
1000
|
|
|
830
1001
|
anthropic_api_key = os.getenv("ANTHROPIC_API_KEY", None)
|
|
831
1002
|
openai_api_key = os.getenv("OPENAI_API_KEY", None)
|
|
1003
|
+
|