npcpy 1.2.33__py3-none-any.whl → 1.2.34__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/memory/command_history.py +26 -1
- npcpy/npc_compiler.py +89 -153
- npcpy/serve.py +362 -291
- {npcpy-1.2.33.dist-info → npcpy-1.2.34.dist-info}/METADATA +1 -1
- {npcpy-1.2.33.dist-info → npcpy-1.2.34.dist-info}/RECORD +8 -8
- {npcpy-1.2.33.dist-info → npcpy-1.2.34.dist-info}/WHEEL +0 -0
- {npcpy-1.2.33.dist-info → npcpy-1.2.34.dist-info}/licenses/LICENSE +0 -0
- {npcpy-1.2.33.dist-info → npcpy-1.2.34.dist-info}/top_level.txt +0 -0
npcpy/memory/command_history.py
CHANGED
|
@@ -825,6 +825,31 @@ class CommandHistory:
|
|
|
825
825
|
FROM message_attachments WHERE message_id = :message_id
|
|
826
826
|
"""
|
|
827
827
|
return self._fetch_all(stmt, {"message_id": message_id})
|
|
828
|
+
def delete_message(self, conversation_id, message_id):
|
|
829
|
+
"""Delete a specific message from a conversation"""
|
|
830
|
+
conn = sqlite3.connect(self.db_path)
|
|
831
|
+
cursor = conn.cursor()
|
|
832
|
+
|
|
833
|
+
try:
|
|
834
|
+
# Delete from the messages table
|
|
835
|
+
cursor.execute("""
|
|
836
|
+
DELETE FROM messages
|
|
837
|
+
WHERE conversation_id = ? AND message_id = ?
|
|
838
|
+
""", (conversation_id, message_id))
|
|
839
|
+
|
|
840
|
+
rows_affected = cursor.rowcount
|
|
841
|
+
conn.commit()
|
|
842
|
+
|
|
843
|
+
print(f"[DB] Deleted message {message_id} from conversation {conversation_id}. Rows affected: {rows_affected}")
|
|
844
|
+
|
|
845
|
+
return rows_affected
|
|
846
|
+
|
|
847
|
+
except Exception as e:
|
|
848
|
+
print(f"[DB] Error deleting message: {e}")
|
|
849
|
+
conn.rollback()
|
|
850
|
+
raise
|
|
851
|
+
finally:
|
|
852
|
+
conn.close()
|
|
828
853
|
|
|
829
854
|
def get_attachment_data(self, attachment_id) -> Optional[Tuple[bytes, str, str]]:
|
|
830
855
|
stmt = "SELECT attachment_data, attachment_name, attachment_type FROM message_attachments WHERE id = :attachment_id"
|
|
@@ -1172,4 +1197,4 @@ def get_available_tables(db_path_or_engine: Union[str, Engine]) -> List[Tuple[st
|
|
|
1172
1197
|
return [row[0] for row in result]
|
|
1173
1198
|
except Exception as e:
|
|
1174
1199
|
print(f"Error getting available tables: {e}")
|
|
1175
|
-
return []
|
|
1200
|
+
return []
|
npcpy/npc_compiler.py
CHANGED
|
@@ -11,6 +11,7 @@ import random
|
|
|
11
11
|
from datetime import datetime
|
|
12
12
|
import hashlib
|
|
13
13
|
import pathlib
|
|
14
|
+
import sys
|
|
14
15
|
import fnmatch
|
|
15
16
|
import subprocess
|
|
16
17
|
from typing import Any, Dict, List, Optional, Union, Callable, Tuple
|
|
@@ -34,6 +35,12 @@ class SilentUndefined(Undefined):
|
|
|
34
35
|
|
|
35
36
|
import math
|
|
36
37
|
from PIL import Image
|
|
38
|
+
from jinja2 import Environment, ChainableUndefined
|
|
39
|
+
|
|
40
|
+
class PreserveUndefined(ChainableUndefined):
|
|
41
|
+
"""Undefined that preserves the original {{ variable }} syntax"""
|
|
42
|
+
def __str__(self):
|
|
43
|
+
return f"{{{{ {self._undefined_name} }}}}"
|
|
37
44
|
|
|
38
45
|
|
|
39
46
|
def agent_pass_handler(command, extracted_data, **kwargs):
|
|
@@ -228,6 +235,7 @@ def write_yaml_file(file_path, data):
|
|
|
228
235
|
print(f"Error writing YAML file {file_path}: {e}")
|
|
229
236
|
return False
|
|
230
237
|
|
|
238
|
+
|
|
231
239
|
class Jinx:
|
|
232
240
|
'''
|
|
233
241
|
Jinx represents a workflow template with Jinja-rendered steps.
|
|
@@ -252,9 +260,8 @@ class Jinx:
|
|
|
252
260
|
else:
|
|
253
261
|
raise ValueError("Either jinx_data or jinx_path must be provided")
|
|
254
262
|
|
|
255
|
-
|
|
256
|
-
self.
|
|
257
|
-
self.steps = [] # This will hold the steps after the first Jinja pass
|
|
263
|
+
self._raw_steps = list(self.steps)
|
|
264
|
+
self.steps = []
|
|
258
265
|
|
|
259
266
|
def _load_from_file(self, path):
|
|
260
267
|
jinx_data = load_yaml_file(path)
|
|
@@ -273,7 +280,7 @@ class Jinx:
|
|
|
273
280
|
self.inputs = jinx_data.get("inputs", [])
|
|
274
281
|
self.description = jinx_data.get("description", "")
|
|
275
282
|
self.npc = jinx_data.get("npc")
|
|
276
|
-
self.steps = jinx_data.get("steps", [])
|
|
283
|
+
self.steps = jinx_data.get("steps", [])
|
|
277
284
|
|
|
278
285
|
def render_first_pass(
|
|
279
286
|
self,
|
|
@@ -301,6 +308,7 @@ class Jinx:
|
|
|
301
308
|
|
|
302
309
|
engine_name = raw_step.get('engine')
|
|
303
310
|
|
|
311
|
+
# If this step references another jinx via engine, expand it
|
|
304
312
|
if engine_name and engine_name in all_jinx_callables:
|
|
305
313
|
step_name = raw_step.get('name', f'call_{engine_name}')
|
|
306
314
|
jinx_args = {
|
|
@@ -314,31 +322,8 @@ class Jinx:
|
|
|
314
322
|
expanded_steps = yaml.safe_load(expanded_yaml_string)
|
|
315
323
|
|
|
316
324
|
if isinstance(expanded_steps, list):
|
|
317
|
-
context_setup_step = {
|
|
318
|
-
'name': f'{step_name}_setup',
|
|
319
|
-
'engine': 'python',
|
|
320
|
-
'code': (
|
|
321
|
-
f'# Setup inputs for {engine_name}\n' +
|
|
322
|
-
'\n'.join([
|
|
323
|
-
f"context['{k}'] = {repr(v)}"
|
|
324
|
-
for k, v in jinx_args.items()
|
|
325
|
-
])
|
|
326
|
-
)
|
|
327
|
-
}
|
|
328
|
-
rendered_steps_output.append(context_setup_step)
|
|
329
325
|
rendered_steps_output.extend(expanded_steps)
|
|
330
326
|
|
|
331
|
-
context_result_step = {
|
|
332
|
-
'name': step_name,
|
|
333
|
-
'engine': 'python',
|
|
334
|
-
'code': (
|
|
335
|
-
f"context['{step_name}'] = "
|
|
336
|
-
f"{{'output': context.get('output', None)}}\n"
|
|
337
|
-
f"output = context['{step_name}']"
|
|
338
|
-
)
|
|
339
|
-
}
|
|
340
|
-
rendered_steps_output.append(context_result_step)
|
|
341
|
-
|
|
342
327
|
elif expanded_steps is not None:
|
|
343
328
|
rendered_steps_output.append(expanded_steps)
|
|
344
329
|
|
|
@@ -349,7 +334,11 @@ class Jinx:
|
|
|
349
334
|
f"(declarative): {e}"
|
|
350
335
|
)
|
|
351
336
|
rendered_steps_output.append(raw_step)
|
|
337
|
+
# Skip rendering for python/bash engine steps - preserve runtime variables
|
|
338
|
+
elif raw_step.get('engine') in ['python', 'bash']:
|
|
339
|
+
rendered_steps_output.append(raw_step)
|
|
352
340
|
else:
|
|
341
|
+
# For other steps, do first-pass rendering (inline macro expansion)
|
|
353
342
|
processed_step = {}
|
|
354
343
|
for key, value in raw_step.items():
|
|
355
344
|
if isinstance(value, str):
|
|
@@ -384,9 +373,8 @@ class Jinx:
|
|
|
384
373
|
npc: Optional[Any] = None,
|
|
385
374
|
messages: Optional[List[Dict[str, str]]] = None,
|
|
386
375
|
extra_globals: Optional[Dict[str, Any]] = None,
|
|
387
|
-
jinja_env: Optional[Environment] = None):
|
|
376
|
+
jinja_env: Optional[Environment] = None):
|
|
388
377
|
|
|
389
|
-
# If jinja_env is not provided, create a default one for the second pass
|
|
390
378
|
if jinja_env is None:
|
|
391
379
|
jinja_env = Environment(
|
|
392
380
|
loader=DictLoader({}),
|
|
@@ -395,7 +383,11 @@ class Jinx:
|
|
|
395
383
|
|
|
396
384
|
active_npc = self.npc if self.npc else npc
|
|
397
385
|
|
|
398
|
-
context = (
|
|
386
|
+
context = (
|
|
387
|
+
active_npc.shared_context.copy()
|
|
388
|
+
if active_npc and hasattr(active_npc, 'shared_context')
|
|
389
|
+
else {}
|
|
390
|
+
)
|
|
399
391
|
context.update(input_values)
|
|
400
392
|
context.update({
|
|
401
393
|
"llm_response": None,
|
|
@@ -404,12 +396,11 @@ class Jinx:
|
|
|
404
396
|
"npc": active_npc
|
|
405
397
|
})
|
|
406
398
|
|
|
407
|
-
# Iterate over self.steps, which are now first-pass rendered
|
|
408
399
|
for i, step in enumerate(self.steps):
|
|
409
400
|
context = self._execute_step(
|
|
410
401
|
step,
|
|
411
402
|
context,
|
|
412
|
-
jinja_env,
|
|
403
|
+
jinja_env,
|
|
413
404
|
npc=active_npc,
|
|
414
405
|
messages=messages,
|
|
415
406
|
extra_globals=extra_globals
|
|
@@ -420,31 +411,49 @@ class Jinx:
|
|
|
420
411
|
def _execute_step(self,
|
|
421
412
|
step: Dict[str, Any],
|
|
422
413
|
context: Dict[str, Any],
|
|
423
|
-
jinja_env: Environment,
|
|
414
|
+
jinja_env: Environment,
|
|
424
415
|
npc: Optional[Any] = None,
|
|
425
416
|
messages: Optional[List[Dict[str, str]]] = None,
|
|
426
417
|
extra_globals: Optional[Dict[str, Any]] = None):
|
|
427
418
|
|
|
428
|
-
|
|
419
|
+
def _log_debug(msg):
|
|
420
|
+
log_file_path = os.path.expanduser("~/jinx_debug_log.txt")
|
|
421
|
+
with open(log_file_path, "a") as f:
|
|
422
|
+
f.write(f"[{datetime.now().isoformat()}] {msg}\n")
|
|
423
|
+
|
|
424
|
+
code_content = step.get("code", "")
|
|
429
425
|
step_name = step.get("name", "unnamed_step")
|
|
430
426
|
step_npc = step.get("npc")
|
|
431
427
|
|
|
432
428
|
active_npc = step_npc if step_npc else npc
|
|
433
429
|
|
|
434
|
-
# Second pass rendering: resolve runtime variables in the code content
|
|
435
430
|
try:
|
|
436
431
|
template = jinja_env.from_string(code_content)
|
|
437
432
|
rendered_code = template.render(**context)
|
|
438
433
|
except Exception as e:
|
|
439
|
-
|
|
440
|
-
|
|
434
|
+
_log_debug(
|
|
435
|
+
f"Error rendering template for step {step_name} "
|
|
436
|
+
f"(second pass): {e}"
|
|
437
|
+
)
|
|
438
|
+
rendered_code = code_content
|
|
441
439
|
|
|
440
|
+
_log_debug(f"rendered jinx code: {rendered_code}")
|
|
441
|
+
_log_debug(
|
|
442
|
+
f"DEBUG: Before exec - rendered_code: {rendered_code}"
|
|
443
|
+
)
|
|
444
|
+
_log_debug(
|
|
445
|
+
f"DEBUG: Before exec - context['output'] before step: "
|
|
446
|
+
f"{context.get('output')}"
|
|
447
|
+
)
|
|
448
|
+
|
|
442
449
|
exec_globals = {
|
|
443
450
|
"__builtins__": __builtins__,
|
|
444
451
|
"npc": active_npc,
|
|
445
452
|
"context": context,
|
|
446
453
|
"pd": pd,
|
|
447
454
|
"plt": plt,
|
|
455
|
+
"sys": sys,
|
|
456
|
+
"subprocess": subprocess,
|
|
448
457
|
"np": np,
|
|
449
458
|
"os": os,
|
|
450
459
|
're': re,
|
|
@@ -465,33 +474,56 @@ class Jinx:
|
|
|
465
474
|
try:
|
|
466
475
|
exec(rendered_code, exec_globals, exec_locals)
|
|
467
476
|
except Exception as e:
|
|
468
|
-
error_msg =
|
|
477
|
+
error_msg = (
|
|
478
|
+
f"Error executing step {step_name}: "
|
|
479
|
+
f"{type(e).__name__}: {e}"
|
|
480
|
+
)
|
|
469
481
|
context['output'] = error_msg
|
|
470
|
-
|
|
482
|
+
_log_debug(error_msg)
|
|
471
483
|
return context
|
|
472
484
|
|
|
485
|
+
_log_debug(f"DEBUG: After exec - exec_locals: {exec_locals}")
|
|
486
|
+
_log_debug(
|
|
487
|
+
f"DEBUG: After exec - 'output' in exec_locals: "
|
|
488
|
+
f"{'output' in exec_locals}"
|
|
489
|
+
)
|
|
490
|
+
|
|
473
491
|
context.update(exec_locals)
|
|
474
492
|
|
|
493
|
+
_log_debug(
|
|
494
|
+
f"DEBUG: After context.update(exec_locals) - "
|
|
495
|
+
f"context['output']: {context.get('output')}"
|
|
496
|
+
)
|
|
497
|
+
_log_debug(f"context after jinx ex: {context}")
|
|
498
|
+
|
|
475
499
|
if "output" in exec_locals:
|
|
476
500
|
outp = exec_locals["output"]
|
|
477
501
|
context["output"] = outp
|
|
478
502
|
context[step_name] = outp
|
|
503
|
+
|
|
504
|
+
_log_debug(
|
|
505
|
+
f"DEBUG: Inside 'output' in exec_locals block - "
|
|
506
|
+
f"context['output']: {context.get('output')}"
|
|
507
|
+
)
|
|
508
|
+
|
|
479
509
|
if messages is not None:
|
|
480
510
|
messages.append({
|
|
481
511
|
'role':'assistant',
|
|
482
|
-
'content':
|
|
512
|
+
'content': (
|
|
513
|
+
f'Jinx {self.jinx_name} step {step_name} '
|
|
514
|
+
f'executed: {outp}'
|
|
515
|
+
)
|
|
483
516
|
})
|
|
484
517
|
context['messages'] = messages
|
|
485
518
|
|
|
486
519
|
return context
|
|
520
|
+
|
|
487
521
|
|
|
488
522
|
def to_dict(self):
|
|
489
523
|
result = {
|
|
490
524
|
"jinx_name": self.jinx_name,
|
|
491
525
|
"description": self.description,
|
|
492
526
|
"inputs": self.inputs,
|
|
493
|
-
# When converting to dict, we should save the *raw* steps
|
|
494
|
-
# so that when reloaded, the first pass can be done again.
|
|
495
527
|
"steps": self._raw_steps
|
|
496
528
|
}
|
|
497
529
|
|
|
@@ -517,8 +549,16 @@ class Jinx:
|
|
|
517
549
|
inputs = []
|
|
518
550
|
for param_name, param in signature.parameters.items():
|
|
519
551
|
if param_name != 'self':
|
|
520
|
-
param_type =
|
|
521
|
-
|
|
552
|
+
param_type = (
|
|
553
|
+
param.annotation
|
|
554
|
+
if param.annotation != inspect.Parameter.empty
|
|
555
|
+
else None
|
|
556
|
+
)
|
|
557
|
+
param_default = (
|
|
558
|
+
None
|
|
559
|
+
if param.default == inspect.Parameter.empty
|
|
560
|
+
else param.default
|
|
561
|
+
)
|
|
522
562
|
|
|
523
563
|
inputs.append({
|
|
524
564
|
"name": param_name,
|
|
@@ -536,7 +576,10 @@ class Jinx:
|
|
|
536
576
|
"code": f"""
|
|
537
577
|
import {mcp_tool.__module__}
|
|
538
578
|
output = {mcp_tool.__module__}.{name}(
|
|
539
|
-
{', '.join([
|
|
579
|
+
{', '.join([
|
|
580
|
+
f'{inp["name"]}=context.get("{inp["name"]}")'
|
|
581
|
+
for inp in inputs
|
|
582
|
+
])}
|
|
540
583
|
)
|
|
541
584
|
"""
|
|
542
585
|
}
|
|
@@ -1392,113 +1435,6 @@ class NPC:
|
|
|
1392
1435
|
response = self.get_llm_response(thinking_prompt, tool_choice = False)
|
|
1393
1436
|
return response.get('response', 'Unable to process thinking request')
|
|
1394
1437
|
|
|
1395
|
-
def write_code(self, task_description: str, language: str = "python", show=True) -> str:
|
|
1396
|
-
"""Generate and execute code for a specific task, returning the result"""
|
|
1397
|
-
if language.lower() != "python":
|
|
1398
|
-
|
|
1399
|
-
code_prompt = f"""Write {language} code for the following task:
|
|
1400
|
-
{task_description}
|
|
1401
|
-
|
|
1402
|
-
Provide clean, working code with brief explanations for key parts:"""
|
|
1403
|
-
|
|
1404
|
-
response = self.get_llm_response(code_prompt, tool_choice=False )
|
|
1405
|
-
return response.get('response', 'Unable to generate code')
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
code_prompt = f"""Write Python code for the following task:
|
|
1409
|
-
{task_description}
|
|
1410
|
-
|
|
1411
|
-
Requirements:
|
|
1412
|
-
- Provide executable Python code
|
|
1413
|
-
- Store the final result in a variable called 'output'
|
|
1414
|
-
- Include any necessary imports
|
|
1415
|
-
- Handle errors gracefully
|
|
1416
|
-
- The code should be ready to execute without modification
|
|
1417
|
-
|
|
1418
|
-
Example format:
|
|
1419
|
-
```python
|
|
1420
|
-
import pandas as pd
|
|
1421
|
-
# Your code here
|
|
1422
|
-
result = some_calculation()
|
|
1423
|
-
output = f"Task completed successfully: {{result}}"
|
|
1424
|
-
"""
|
|
1425
|
-
response = self.get_llm_response(code_prompt, tool_choice= False)
|
|
1426
|
-
generated_code = response.get('response', '')
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
if '```python' in generated_code:
|
|
1430
|
-
code_lines = generated_code.split('\n')
|
|
1431
|
-
start_idx = None
|
|
1432
|
-
end_idx = None
|
|
1433
|
-
|
|
1434
|
-
for i, line in enumerate(code_lines):
|
|
1435
|
-
if '```python' in line:
|
|
1436
|
-
start_idx = i + 1
|
|
1437
|
-
elif '```' in line and start_idx is not None:
|
|
1438
|
-
end_idx = i
|
|
1439
|
-
break
|
|
1440
|
-
|
|
1441
|
-
if start_idx is not None:
|
|
1442
|
-
if end_idx is not None:
|
|
1443
|
-
generated_code = '\n'.join(code_lines[start_idx:end_idx])
|
|
1444
|
-
else:
|
|
1445
|
-
generated_code = '\n'.join(code_lines[start_idx:])
|
|
1446
|
-
|
|
1447
|
-
try:
|
|
1448
|
-
|
|
1449
|
-
exec_globals = {
|
|
1450
|
-
"__builtins__": __builtins__,
|
|
1451
|
-
"npc": self,
|
|
1452
|
-
"context": self.shared_context,
|
|
1453
|
-
"pd": pd,
|
|
1454
|
-
"plt": plt,
|
|
1455
|
-
"np": np,
|
|
1456
|
-
"os": os,
|
|
1457
|
-
"re": re,
|
|
1458
|
-
"json": json,
|
|
1459
|
-
"Path": pathlib.Path,
|
|
1460
|
-
"fnmatch": fnmatch,
|
|
1461
|
-
"pathlib": pathlib,
|
|
1462
|
-
"subprocess": subprocess,
|
|
1463
|
-
"datetime": datetime,
|
|
1464
|
-
"hashlib": hashlib,
|
|
1465
|
-
"sqlite3": sqlite3,
|
|
1466
|
-
"yaml": yaml,
|
|
1467
|
-
"random": random,
|
|
1468
|
-
"math": math,
|
|
1469
|
-
}
|
|
1470
|
-
|
|
1471
|
-
exec_locals = {}
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
exec(generated_code, exec_globals, exec_locals)
|
|
1475
|
-
|
|
1476
|
-
if show:
|
|
1477
|
-
print('Executing code', generated_code)
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
if "output" in exec_locals:
|
|
1481
|
-
result = exec_locals["output"]
|
|
1482
|
-
|
|
1483
|
-
self.shared_context.update({k: v for k, v in exec_locals.items()
|
|
1484
|
-
if not k.startswith('_') and not callable(v)})
|
|
1485
|
-
return f"Code executed successfully. Result: {result}"
|
|
1486
|
-
else:
|
|
1487
|
-
|
|
1488
|
-
meaningful_vars = {k: v for k, v in exec_locals.items()
|
|
1489
|
-
if not k.startswith('_') and not callable(v)}
|
|
1490
|
-
|
|
1491
|
-
self.shared_context.update(meaningful_vars)
|
|
1492
|
-
|
|
1493
|
-
if meaningful_vars:
|
|
1494
|
-
last_var = list(meaningful_vars.items())[-1]
|
|
1495
|
-
return f"Code executed successfully. Last result: {last_var[0]} = {last_var[1]}"
|
|
1496
|
-
else:
|
|
1497
|
-
return "Code executed successfully (no explicit output generated)"
|
|
1498
|
-
|
|
1499
|
-
except Exception as e:
|
|
1500
|
-
error_msg = f"Error executing code: {str(e)}\n\nGenerated code was:\n{generated_code}"
|
|
1501
|
-
return error_msg
|
|
1502
1438
|
|
|
1503
1439
|
|
|
1504
1440
|
|
|
@@ -2634,4 +2570,4 @@ class Team:
|
|
|
2634
2570
|
context_parts.append(content)
|
|
2635
2571
|
context_parts.append("")
|
|
2636
2572
|
|
|
2637
|
-
return "\n".join(context_parts)
|
|
2573
|
+
return "\n".join(context_parts)
|
npcpy/serve.py
CHANGED
|
@@ -7,8 +7,9 @@ import uuid
|
|
|
7
7
|
import sys
|
|
8
8
|
import traceback
|
|
9
9
|
import glob
|
|
10
|
+
import re
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
import io
|
|
12
13
|
from flask_cors import CORS
|
|
13
14
|
import os
|
|
14
15
|
import sqlite3
|
|
@@ -29,6 +30,20 @@ try:
|
|
|
29
30
|
import ollama
|
|
30
31
|
except:
|
|
31
32
|
pass
|
|
33
|
+
from jinja2 import Environment, FileSystemLoader, Template, Undefined, DictLoader
|
|
34
|
+
class SilentUndefined(Undefined):
|
|
35
|
+
def _fail_with_undefined_error(self, *args, **kwargs):
|
|
36
|
+
return ""
|
|
37
|
+
|
|
38
|
+
# Import ShellState and helper functions from npcsh
|
|
39
|
+
from npcsh._state import ShellState
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
from npcpy.memory.knowledge_graph import load_kg_from_db
|
|
43
|
+
from npcpy.memory.search import execute_rag_command, execute_brainblast_command
|
|
44
|
+
from npcpy.data.load import load_file_contents
|
|
45
|
+
from npcpy.data.web import search_web
|
|
46
|
+
from npcsh._state import get_relevant_memories, search_kg_facts
|
|
32
47
|
|
|
33
48
|
import base64
|
|
34
49
|
import shutil
|
|
@@ -535,46 +550,63 @@ def get_available_jinxs():
|
|
|
535
550
|
traceback.print_exc()
|
|
536
551
|
return jsonify({'jinxs': [], 'error': str(e)}), 500
|
|
537
552
|
|
|
538
|
-
@app.route(
|
|
553
|
+
@app.route('/api/jinxs/global', methods=['GET'])
|
|
539
554
|
def get_global_jinxs():
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
555
|
+
global_jinxs_dir = os.path.expanduser('~/.npcsh/npc_team/jinxs')
|
|
556
|
+
|
|
557
|
+
# Directories to exclude entirely
|
|
558
|
+
excluded_dirs = ['core', 'npc_studio']
|
|
559
|
+
|
|
560
|
+
code_jinxs = []
|
|
561
|
+
mode_jinxs = []
|
|
562
|
+
util_jinxs = []
|
|
563
|
+
|
|
564
|
+
if os.path.exists(global_jinxs_dir):
|
|
565
|
+
for root, dirs, files in os.walk(global_jinxs_dir):
|
|
566
|
+
# Filter out excluded directories
|
|
567
|
+
dirs[:] = [d for d in dirs if d not in excluded_dirs]
|
|
568
|
+
|
|
569
|
+
for filename in files:
|
|
570
|
+
if filename.endswith('.jinx'):
|
|
571
|
+
try:
|
|
572
|
+
jinx_path = os.path.join(root, filename)
|
|
573
|
+
with open(jinx_path, 'r') as f:
|
|
574
|
+
jinx_data = yaml.safe_load(f)
|
|
575
|
+
|
|
576
|
+
if jinx_data:
|
|
577
|
+
jinx_name = jinx_data.get('jinx_name', filename[:-5])
|
|
578
|
+
|
|
579
|
+
jinx_obj = {
|
|
580
|
+
'name': jinx_name,
|
|
581
|
+
'display_name': jinx_data.get('description', jinx_name),
|
|
582
|
+
'description': jinx_data.get('description', ''),
|
|
583
|
+
'inputs': jinx_data.get('inputs', []),
|
|
584
|
+
'path': jinx_path
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
# Categorize based on directory
|
|
588
|
+
rel_path = os.path.relpath(root, global_jinxs_dir)
|
|
589
|
+
|
|
590
|
+
if rel_path.startswith('code'):
|
|
591
|
+
code_jinxs.append(jinx_obj)
|
|
592
|
+
elif rel_path.startswith('modes'):
|
|
593
|
+
mode_jinxs.append(jinx_obj)
|
|
594
|
+
elif rel_path.startswith('utils'):
|
|
595
|
+
util_jinxs.append(jinx_obj)
|
|
596
|
+
|
|
597
|
+
except Exception as e:
|
|
598
|
+
print(f"Error loading jinx {filename}: {e}")
|
|
599
|
+
|
|
600
|
+
return jsonify({
|
|
601
|
+
'code': code_jinxs,
|
|
602
|
+
'modes': mode_jinxs,
|
|
603
|
+
'utils': util_jinxs
|
|
604
|
+
})
|
|
573
605
|
@app.route("/api/jinx/execute", methods=["POST"])
|
|
574
606
|
def execute_jinx():
|
|
575
607
|
"""
|
|
576
608
|
Execute a specific jinx with provided arguments.
|
|
577
|
-
|
|
609
|
+
Returns the output as a JSON response.
|
|
578
610
|
"""
|
|
579
611
|
data = request.json
|
|
580
612
|
|
|
@@ -585,19 +617,18 @@ def execute_jinx():
|
|
|
585
617
|
with cancellation_lock:
|
|
586
618
|
cancellation_flags[stream_id] = False
|
|
587
619
|
|
|
588
|
-
print(f"--- Jinx Execution Request for streamId: {stream_id} ---")
|
|
589
|
-
print(f"Request Data: {json.dumps(data, indent=2)}")
|
|
620
|
+
print(f"--- Jinx Execution Request for streamId: {stream_id} ---", file=sys.stderr)
|
|
621
|
+
print(f"Request Data: {json.dumps(data, indent=2)}", file=sys.stderr)
|
|
590
622
|
|
|
591
623
|
jinx_name = data.get("jinxName")
|
|
592
624
|
jinx_args = data.get("jinxArgs", [])
|
|
593
|
-
print(f"Jinx Name: {jinx_name}, Jinx Args: {jinx_args}")
|
|
625
|
+
print(f"Jinx Name: {jinx_name}, Jinx Args: {jinx_args}", file=sys.stderr)
|
|
594
626
|
conversation_id = data.get("conversationId")
|
|
595
627
|
model = data.get("model")
|
|
596
628
|
provider = data.get("provider")
|
|
597
629
|
|
|
598
|
-
# --- IMPORTANT: Ensure conversation_id is present for context persistence ---
|
|
599
630
|
if not conversation_id:
|
|
600
|
-
print("ERROR: conversationId is required for Jinx execution with persistent variables")
|
|
631
|
+
print("ERROR: conversationId is required for Jinx execution with persistent variables", file=sys.stderr)
|
|
601
632
|
return jsonify({"error": "conversationId is required for Jinx execution with persistent variables"}), 400
|
|
602
633
|
|
|
603
634
|
npc_name = data.get("npc")
|
|
@@ -605,222 +636,194 @@ def execute_jinx():
|
|
|
605
636
|
current_path = data.get("currentPath")
|
|
606
637
|
|
|
607
638
|
if not jinx_name:
|
|
608
|
-
print("ERROR: jinxName is required")
|
|
639
|
+
print("ERROR: jinxName is required", file=sys.stderr)
|
|
609
640
|
return jsonify({"error": "jinxName is required"}), 400
|
|
610
641
|
|
|
611
|
-
# Load project environment if applicable
|
|
612
642
|
if current_path:
|
|
613
643
|
load_project_env(current_path)
|
|
614
644
|
|
|
615
|
-
|
|
616
|
-
|
|
645
|
+
jinx = None
|
|
646
|
+
|
|
617
647
|
if npc_name:
|
|
618
648
|
db_conn = get_db_connection()
|
|
619
649
|
npc_object = load_npc_by_name_and_source(npc_name, npc_source, db_conn, current_path)
|
|
620
650
|
if not npc_object and npc_source == 'project':
|
|
621
651
|
npc_object = load_npc_by_name_and_source(npc_name, 'global', db_conn)
|
|
652
|
+
else:
|
|
653
|
+
npc_object = None
|
|
622
654
|
|
|
623
|
-
# Try to find the jinx
|
|
624
|
-
jinx = None
|
|
625
|
-
|
|
626
|
-
# Check NPC's jinxs
|
|
627
655
|
if npc_object and hasattr(npc_object, 'jinxs_dict') and jinx_name in npc_object.jinxs_dict:
|
|
628
656
|
jinx = npc_object.jinxs_dict[jinx_name]
|
|
657
|
+
print(f"Found jinx in NPC's jinxs_dict", file=sys.stderr)
|
|
629
658
|
|
|
630
|
-
# Check team jinxs
|
|
631
659
|
if not jinx and current_path:
|
|
632
|
-
|
|
633
|
-
if os.path.exists(
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
660
|
+
project_jinxs_base = os.path.join(current_path, 'npc_team', 'jinxs')
|
|
661
|
+
if os.path.exists(project_jinxs_base):
|
|
662
|
+
for root, dirs, files in os.walk(project_jinxs_base):
|
|
663
|
+
if f'{jinx_name}.jinx' in files:
|
|
664
|
+
project_jinx_path = os.path.join(root, f'{jinx_name}.jinx')
|
|
665
|
+
jinx = Jinx(jinx_path=project_jinx_path)
|
|
666
|
+
print(f"Found jinx at: {project_jinx_path}", file=sys.stderr)
|
|
667
|
+
break
|
|
668
|
+
|
|
637
669
|
if not jinx:
|
|
638
|
-
|
|
639
|
-
if os.path.exists(
|
|
640
|
-
|
|
670
|
+
global_jinxs_base = os.path.expanduser('~/.npcsh/npc_team/jinxs')
|
|
671
|
+
if os.path.exists(global_jinxs_base):
|
|
672
|
+
for root, dirs, files in os.walk(global_jinxs_base):
|
|
673
|
+
if f'{jinx_name}.jinx' in files:
|
|
674
|
+
global_jinx_path = os.path.join(root, f'{jinx_name}.jinx')
|
|
675
|
+
jinx = Jinx(jinx_path=global_jinx_path)
|
|
676
|
+
print(f"Found jinx at: {global_jinx_path}", file=sys.stderr)
|
|
677
|
+
|
|
678
|
+
# Initialize jinx steps by calling render_first_pass
|
|
679
|
+
from jinja2 import Environment
|
|
680
|
+
temp_env = Environment()
|
|
681
|
+
jinx.render_first_pass(temp_env, {})
|
|
682
|
+
|
|
683
|
+
break
|
|
641
684
|
|
|
642
685
|
if not jinx:
|
|
643
|
-
print(f"ERROR: Jinx '{jinx_name}' not found")
|
|
686
|
+
print(f"ERROR: Jinx '{jinx_name}' not found", file=sys.stderr)
|
|
687
|
+
searched_paths = []
|
|
688
|
+
if npc_object:
|
|
689
|
+
searched_paths.append(f"NPC {npc_name} jinxs_dict")
|
|
690
|
+
if current_path:
|
|
691
|
+
searched_paths.append(f"Project jinxs at {os.path.join(current_path, 'npc_team', 'jinxs')}")
|
|
692
|
+
searched_paths.append(f"Global jinxs at {os.path.expanduser('~/.npcsh/npc_team/jinxs')}")
|
|
693
|
+
print(f"Searched in: {', '.join(searched_paths)}", file=sys.stderr)
|
|
644
694
|
return jsonify({"error": f"Jinx '{jinx_name}' not found"}), 404
|
|
645
695
|
|
|
646
|
-
# Extract inputs from args
|
|
647
696
|
from npcpy.npc_compiler import extract_jinx_inputs
|
|
648
697
|
|
|
649
|
-
# Re-assemble arguments that were incorrectly split by spaces.
|
|
650
698
|
fixed_args = []
|
|
651
699
|
i = 0
|
|
652
|
-
|
|
653
|
-
|
|
700
|
+
|
|
701
|
+
# Filter out None values from jinx_args before processing
|
|
702
|
+
cleaned_jinx_args = [arg for arg in jinx_args if arg is not None]
|
|
703
|
+
|
|
704
|
+
while i < len(cleaned_jinx_args):
|
|
705
|
+
arg = cleaned_jinx_args[i]
|
|
654
706
|
if arg.startswith('-'):
|
|
655
707
|
fixed_args.append(arg)
|
|
656
708
|
value_parts = []
|
|
657
709
|
i += 1
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
value_parts.append(jinx_args[i])
|
|
710
|
+
while i < len(cleaned_jinx_args) and not cleaned_jinx_args[i].startswith('-'):
|
|
711
|
+
value_parts.append(cleaned_jinx_args[i])
|
|
661
712
|
i += 1
|
|
662
713
|
|
|
663
714
|
if value_parts:
|
|
664
|
-
# Join the parts back into a single string.
|
|
665
715
|
full_value = " ".join(value_parts)
|
|
666
|
-
# Clean up the extraneous quotes that the initial bad split left behind.
|
|
667
716
|
if full_value.startswith("'") and full_value.endswith("'"):
|
|
668
717
|
full_value = full_value[1:-1]
|
|
669
718
|
elif full_value.startswith('"') and full_value.endswith('"'):
|
|
670
719
|
full_value = full_value[1:-1]
|
|
671
720
|
fixed_args.append(full_value)
|
|
672
|
-
# The 'i' counter is already advanced, so the loop continues from the next flag.
|
|
673
721
|
else:
|
|
674
|
-
# This handles positional arguments, just in case.
|
|
675
722
|
fixed_args.append(arg)
|
|
676
723
|
i += 1
|
|
677
724
|
|
|
678
|
-
# Now, use the corrected arguments to extract inputs.
|
|
679
725
|
input_values = extract_jinx_inputs(fixed_args, jinx)
|
|
680
726
|
|
|
681
|
-
print(f'Executing jinx with input_values: {input_values}')
|
|
682
|
-
|
|
727
|
+
print(f'Executing jinx with input_values: {input_values}', file=sys.stderr)
|
|
728
|
+
|
|
683
729
|
command_history = CommandHistory(app.config.get('DB_PATH'))
|
|
684
730
|
messages = fetch_messages_for_conversation(conversation_id)
|
|
685
731
|
|
|
686
|
-
# Prepare jinxs_dict for execution
|
|
687
732
|
all_jinxs = {}
|
|
688
733
|
if npc_object and hasattr(npc_object, 'jinxs_dict'):
|
|
689
734
|
all_jinxs.update(npc_object.jinxs_dict)
|
|
690
735
|
|
|
691
|
-
# --- IMPORTANT: Retrieve or initialize the persistent Jinx context for this conversation ---
|
|
692
736
|
if conversation_id not in app.jinx_conversation_contexts:
|
|
693
737
|
app.jinx_conversation_contexts[conversation_id] = {}
|
|
694
738
|
jinx_local_context = app.jinx_conversation_contexts[conversation_id]
|
|
695
739
|
|
|
696
|
-
print(f"--- CONTEXT STATE (conversationId: {conversation_id}) ---")
|
|
697
|
-
print(f"jinx_local_context BEFORE Jinx execution: {jinx_local_context}")
|
|
740
|
+
print(f"--- CONTEXT STATE (conversationId: {conversation_id}) ---", file=sys.stderr)
|
|
741
|
+
print(f"jinx_local_context BEFORE Jinx execution: {jinx_local_context}", file=sys.stderr)
|
|
698
742
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
# Get output (this still comes from the 'output' key in the result)
|
|
728
|
-
output = result.get('output', str(result))
|
|
729
|
-
messages_updated = result.get('messages', messages)
|
|
743
|
+
|
|
744
|
+
# Create state object
|
|
745
|
+
state = ShellState(
|
|
746
|
+
npc=npc_object,
|
|
747
|
+
team=None,
|
|
748
|
+
conversation_id=conversation_id,
|
|
749
|
+
chat_model=model or os.getenv('NPCSH_CHAT_MODEL', 'gemma3:4b'),
|
|
750
|
+
chat_provider=provider or os.getenv('NPCSH_CHAT_PROVIDER', 'ollama'),
|
|
751
|
+
current_path=current_path or os.getcwd(),
|
|
752
|
+
search_provider=os.getenv('NPCSH_SEARCH_PROVIDER', 'duckduckgo'),
|
|
753
|
+
embedding_model=os.getenv('NPCSH_EMBEDDING_MODEL', 'nomic-embed-text'),
|
|
754
|
+
embedding_provider=os.getenv('NPCSH_EMBEDDING_PROVIDER', 'ollama'),
|
|
755
|
+
)
|
|
756
|
+
|
|
757
|
+
# Build extra_globals with state and all necessary functions
|
|
758
|
+
extra_globals_for_jinx = {
|
|
759
|
+
**jinx_local_context,
|
|
760
|
+
'state': state,
|
|
761
|
+
'CommandHistory': CommandHistory,
|
|
762
|
+
'load_kg_from_db': load_kg_from_db,
|
|
763
|
+
'execute_rag_command': execute_rag_command,
|
|
764
|
+
'execute_brainblast_command': execute_brainblast_command,
|
|
765
|
+
'load_file_contents': load_file_contents,
|
|
766
|
+
'search_web': search_web,
|
|
767
|
+
'get_relevant_memories': get_relevant_memories,
|
|
768
|
+
'search_kg_facts': search_kg_facts,
|
|
769
|
+
}
|
|
730
770
|
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
"user",
|
|
787
|
-
f"/{jinx_name} {' '.join(jinx_args)}",
|
|
788
|
-
wd=current_path,
|
|
789
|
-
model=model,
|
|
790
|
-
provider=provider,
|
|
791
|
-
npc=npc_name,
|
|
792
|
-
message_id=message_id
|
|
793
|
-
)
|
|
794
|
-
|
|
795
|
-
message_id = generate_message_id()
|
|
796
|
-
save_conversation_message(
|
|
797
|
-
command_history,
|
|
798
|
-
conversation_id,
|
|
799
|
-
"assistant",
|
|
800
|
-
str(output),
|
|
801
|
-
wd=current_path,
|
|
802
|
-
model=model,
|
|
803
|
-
provider=provider,
|
|
804
|
-
npc=npc_name,
|
|
805
|
-
message_id=message_id
|
|
806
|
-
)
|
|
807
|
-
|
|
808
|
-
except Exception as e:
|
|
809
|
-
print(f"ERROR: Exception during jinx execution {jinx_name}: {str(e)}")
|
|
810
|
-
traceback.print_exc()
|
|
811
|
-
error_data = {
|
|
812
|
-
"type": "error",
|
|
813
|
-
"error": str(e)
|
|
814
|
-
}
|
|
815
|
-
yield f"data: {json.dumps(error_data)}\n\n"
|
|
816
|
-
|
|
817
|
-
finally:
|
|
818
|
-
with cancellation_lock:
|
|
819
|
-
if current_stream_id in cancellation_flags:
|
|
820
|
-
del cancellation_flags[current_stream_id]
|
|
821
|
-
print(f"--- Jinx Execution Finished for streamId: {stream_id} ---")
|
|
771
|
+
jinx_execution_result = jinx.execute(
|
|
772
|
+
input_values=input_values,
|
|
773
|
+
jinja_env=npc_object.jinja_env if npc_object else None,
|
|
774
|
+
npc=npc_object,
|
|
775
|
+
messages=messages,
|
|
776
|
+
extra_globals=extra_globals_for_jinx
|
|
777
|
+
)
|
|
778
|
+
|
|
779
|
+
output_from_jinx_result = jinx_execution_result.get('output')
|
|
780
|
+
|
|
781
|
+
final_output_string = str(output_from_jinx_result) if output_from_jinx_result is not None else ""
|
|
782
|
+
|
|
783
|
+
if isinstance(jinx_execution_result, dict):
|
|
784
|
+
for key, value in jinx_execution_result.items():
|
|
785
|
+
jinx_local_context[key] = value
|
|
786
|
+
|
|
787
|
+
print(f"jinx_local_context AFTER Jinx execution (final state): {jinx_local_context}", file=sys.stderr)
|
|
788
|
+
print(f"Jinx execution result output: {output_from_jinx_result}", file=sys.stderr)
|
|
789
|
+
|
|
790
|
+
user_message_id = generate_message_id()
|
|
791
|
+
|
|
792
|
+
# Use cleaned_jinx_args for logging the user message
|
|
793
|
+
user_command_log = f"/{jinx_name} {' '.join(cleaned_jinx_args)}"
|
|
794
|
+
save_conversation_message(
|
|
795
|
+
command_history,
|
|
796
|
+
conversation_id,
|
|
797
|
+
"user",
|
|
798
|
+
user_command_log,
|
|
799
|
+
wd=current_path,
|
|
800
|
+
model=model,
|
|
801
|
+
provider=provider,
|
|
802
|
+
npc=npc_name,
|
|
803
|
+
message_id=user_message_id
|
|
804
|
+
)
|
|
805
|
+
|
|
806
|
+
assistant_message_id = generate_message_id()
|
|
807
|
+
save_conversation_message(
|
|
808
|
+
command_history,
|
|
809
|
+
conversation_id,
|
|
810
|
+
"assistant",
|
|
811
|
+
final_output_string,
|
|
812
|
+
wd=current_path,
|
|
813
|
+
model=model,
|
|
814
|
+
provider=provider,
|
|
815
|
+
npc=npc_name,
|
|
816
|
+
message_id=assistant_message_id
|
|
817
|
+
)
|
|
818
|
+
|
|
819
|
+
# Determine mimetype based on content
|
|
820
|
+
is_html = bool(re.search(r'<[a-z][\s\S]*>', final_output_string, re.IGNORECASE))
|
|
821
|
+
|
|
822
|
+
if is_html:
|
|
823
|
+
return Response(final_output_string, mimetype="text/html")
|
|
824
|
+
else:
|
|
825
|
+
return Response(final_output_string, mimetype="text/plain")
|
|
822
826
|
|
|
823
|
-
return Response(event_stream(stream_id), mimetype="text/event-stream")
|
|
824
827
|
|
|
825
828
|
@app.route("/api/settings/global", methods=["POST", "OPTIONS"])
|
|
826
829
|
def save_global_settings():
|
|
@@ -1009,52 +1012,6 @@ def api_command(command):
|
|
|
1009
1012
|
return jsonify(result)
|
|
1010
1013
|
except Exception as e:
|
|
1011
1014
|
return jsonify({"error": str(e)})
|
|
1012
|
-
@app.route("/api/npc_team_global")
|
|
1013
|
-
def get_npc_team_global():
|
|
1014
|
-
try:
|
|
1015
|
-
db_conn = get_db_connection()
|
|
1016
|
-
global_npc_directory = os.path.expanduser("~/.npcsh/npc_team")
|
|
1017
|
-
|
|
1018
|
-
npc_data = []
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
for file in os.listdir(global_npc_directory):
|
|
1022
|
-
if file.endswith(".npc"):
|
|
1023
|
-
npc_path = os.path.join(global_npc_directory, file)
|
|
1024
|
-
npc = NPC(file=npc_path, db_conn=db_conn)
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
serialized_npc = {
|
|
1028
|
-
"name": npc.name,
|
|
1029
|
-
"primary_directive": npc.primary_directive,
|
|
1030
|
-
"model": npc.model,
|
|
1031
|
-
"provider": npc.provider,
|
|
1032
|
-
"api_url": npc.api_url,
|
|
1033
|
-
"use_global_jinxs": npc.use_global_jinxs,
|
|
1034
|
-
"jinxs": [
|
|
1035
|
-
{
|
|
1036
|
-
"jinx_name": jinx.jinx_name,
|
|
1037
|
-
"inputs": jinx.inputs,
|
|
1038
|
-
"steps": [
|
|
1039
|
-
{
|
|
1040
|
-
"name": step.get("name", f"step_{i}"),
|
|
1041
|
-
"engine": step.get("engine", "natural"),
|
|
1042
|
-
"code": step.get("code", "")
|
|
1043
|
-
}
|
|
1044
|
-
for i, step in enumerate(jinx.steps)
|
|
1045
|
-
]
|
|
1046
|
-
}
|
|
1047
|
-
for jinx in npc.jinxs
|
|
1048
|
-
],
|
|
1049
|
-
}
|
|
1050
|
-
npc_data.append(serialized_npc)
|
|
1051
|
-
|
|
1052
|
-
return jsonify({"npcs": npc_data, "error": None})
|
|
1053
|
-
|
|
1054
|
-
except Exception as e:
|
|
1055
|
-
print(f"Error loading global NPCs: {str(e)}")
|
|
1056
|
-
return jsonify({"npcs": [], "error": str(e)})
|
|
1057
|
-
|
|
1058
1015
|
|
|
1059
1016
|
@app.route("/api/jinxs/save", methods=["POST"])
|
|
1060
1017
|
def save_jinx():
|
|
@@ -1135,6 +1092,67 @@ use_global_jinxs: {str(npc_data.get('use_global_jinxs', True)).lower()}
|
|
|
1135
1092
|
print(f"Error saving NPC: {str(e)}")
|
|
1136
1093
|
return jsonify({"error": str(e)}), 500
|
|
1137
1094
|
|
|
1095
|
+
@app.route("/api/npc_team_global")
|
|
1096
|
+
def get_npc_team_global():
|
|
1097
|
+
try:
|
|
1098
|
+
db_conn = get_db_connection()
|
|
1099
|
+
global_npc_directory = os.path.expanduser("~/.npcsh/npc_team")
|
|
1100
|
+
|
|
1101
|
+
npc_data = []
|
|
1102
|
+
|
|
1103
|
+
# Ensure the directory exists before listing
|
|
1104
|
+
if not os.path.exists(global_npc_directory):
|
|
1105
|
+
print(f"Global NPC directory not found: {global_npc_directory}", file=sys.stderr)
|
|
1106
|
+
return jsonify({"npcs": [], "error": f"Global NPC directory not found: {global_npc_directory}"})
|
|
1107
|
+
|
|
1108
|
+
for file in os.listdir(global_npc_directory):
|
|
1109
|
+
if file.endswith(".npc"):
|
|
1110
|
+
npc_path = os.path.join(global_npc_directory, file)
|
|
1111
|
+
try:
|
|
1112
|
+
npc = NPC(file=npc_path, db_conn=db_conn)
|
|
1113
|
+
|
|
1114
|
+
# Ensure jinxs are initialized after NPC creation if not already
|
|
1115
|
+
# This is crucial for populating npc.jinxs_dict
|
|
1116
|
+
if not npc.jinxs_dict and hasattr(npc, 'initialize_jinxs'):
|
|
1117
|
+
npc.initialize_jinxs()
|
|
1118
|
+
|
|
1119
|
+
serialized_npc = {
|
|
1120
|
+
"name": npc.name,
|
|
1121
|
+
"primary_directive": npc.primary_directive,
|
|
1122
|
+
"model": npc.model,
|
|
1123
|
+
"provider": npc.provider,
|
|
1124
|
+
"api_url": npc.api_url,
|
|
1125
|
+
"use_global_jinxs": npc.use_global_jinxs,
|
|
1126
|
+
# CRITICAL FIX: Iterate over npc.jinxs_dict.values() which contains Jinx objects
|
|
1127
|
+
"jinxs": [
|
|
1128
|
+
{
|
|
1129
|
+
"jinx_name": jinx.jinx_name,
|
|
1130
|
+
"inputs": jinx.inputs,
|
|
1131
|
+
"steps": [
|
|
1132
|
+
{
|
|
1133
|
+
"name": step.get("name", f"step_{i}"),
|
|
1134
|
+
"engine": step.get("engine", "natural"),
|
|
1135
|
+
"code": step.get("code", "")
|
|
1136
|
+
}
|
|
1137
|
+
for i, step in enumerate(jinx.steps)
|
|
1138
|
+
]
|
|
1139
|
+
}
|
|
1140
|
+
for jinx in npc.jinxs_dict.values() # Use jinxs_dict here
|
|
1141
|
+
] if hasattr(npc, 'jinxs_dict') else [], # Defensive check
|
|
1142
|
+
}
|
|
1143
|
+
npc_data.append(serialized_npc)
|
|
1144
|
+
except Exception as e:
|
|
1145
|
+
print(f"Error loading or serializing NPC {file}: {str(e)}", file=sys.stderr)
|
|
1146
|
+
traceback.print_exc(file=sys.stderr)
|
|
1147
|
+
|
|
1148
|
+
|
|
1149
|
+
return jsonify({"npcs": npc_data, "error": None})
|
|
1150
|
+
|
|
1151
|
+
except Exception as e:
|
|
1152
|
+
print(f"Error fetching global NPC team: {str(e)}", file=sys.stderr)
|
|
1153
|
+
traceback.print_exc(file=sys.stderr)
|
|
1154
|
+
return jsonify({"npcs": [], "error": str(e)})
|
|
1155
|
+
|
|
1138
1156
|
|
|
1139
1157
|
@app.route("/api/npc_team_project", methods=["GET"])
|
|
1140
1158
|
def get_npc_team_project():
|
|
@@ -1142,49 +1160,70 @@ def get_npc_team_project():
|
|
|
1142
1160
|
db_conn = get_db_connection()
|
|
1143
1161
|
|
|
1144
1162
|
project_npc_directory = request.args.get("currentPath")
|
|
1163
|
+
if not project_npc_directory:
|
|
1164
|
+
return jsonify({"npcs": [], "error": "currentPath is required for project NPCs"}), 400
|
|
1165
|
+
|
|
1145
1166
|
if not project_npc_directory.endswith("npc_team"):
|
|
1146
1167
|
project_npc_directory = os.path.join(project_npc_directory, "npc_team")
|
|
1147
1168
|
|
|
1148
1169
|
npc_data = []
|
|
1149
1170
|
|
|
1171
|
+
# Ensure the directory exists before listing
|
|
1172
|
+
if not os.path.exists(project_npc_directory):
|
|
1173
|
+
print(f"Project NPC directory not found: {project_npc_directory}", file=sys.stderr)
|
|
1174
|
+
return jsonify({"npcs": [], "error": f"Project NPC directory not found: {project_npc_directory}"})
|
|
1175
|
+
|
|
1150
1176
|
for file in os.listdir(project_npc_directory):
|
|
1151
|
-
print(file)
|
|
1177
|
+
print(f"Processing project NPC file: {file}", file=sys.stderr) # Diagnostic print
|
|
1152
1178
|
if file.endswith(".npc"):
|
|
1153
1179
|
npc_path = os.path.join(project_npc_directory, file)
|
|
1154
|
-
|
|
1180
|
+
try:
|
|
1181
|
+
npc = NPC(file=npc_path, db_conn=db_conn)
|
|
1182
|
+
|
|
1183
|
+
# Ensure jinxs are initialized after NPC creation if not already
|
|
1184
|
+
# This is crucial for populating npc.jinxs_dict
|
|
1185
|
+
if not npc.jinxs_dict and hasattr(npc, 'initialize_jinxs'):
|
|
1186
|
+
npc.initialize_jinxs()
|
|
1187
|
+
|
|
1188
|
+
serialized_npc = {
|
|
1189
|
+
"name": npc.name,
|
|
1190
|
+
"primary_directive": npc.primary_directive,
|
|
1191
|
+
"model": npc.model,
|
|
1192
|
+
"provider": npc.provider,
|
|
1193
|
+
"api_url": npc.api_url,
|
|
1194
|
+
"use_global_jinxs": npc.use_global_jinxs,
|
|
1195
|
+
# CRITICAL FIX: Iterate over npc.jinxs_dict.values() which contains Jinx objects
|
|
1196
|
+
"jinxs": [
|
|
1197
|
+
{
|
|
1198
|
+
"jinx_name": jinx.jinx_name,
|
|
1199
|
+
"inputs": jinx.inputs,
|
|
1200
|
+
"steps": [
|
|
1201
|
+
{
|
|
1202
|
+
"name": step.get("name", f"step_{i}"),
|
|
1203
|
+
"engine": step.get("engine", "natural"),
|
|
1204
|
+
"code": step.get("code", "")
|
|
1205
|
+
}
|
|
1206
|
+
for i, step in enumerate(jinx.steps)
|
|
1207
|
+
]
|
|
1208
|
+
}
|
|
1209
|
+
for jinx in npc.jinxs_dict.values() # Use jinxs_dict here
|
|
1210
|
+
] if hasattr(npc, 'jinxs_dict') else [], # Defensive check
|
|
1211
|
+
}
|
|
1212
|
+
npc_data.append(serialized_npc)
|
|
1213
|
+
except Exception as e:
|
|
1214
|
+
print(f"Error loading or serializing NPC {file}: {str(e)}", file=sys.stderr)
|
|
1215
|
+
traceback.print_exc(file=sys.stderr)
|
|
1155
1216
|
|
|
1156
|
-
|
|
1157
|
-
serialized_npc = {
|
|
1158
|
-
"name": npc.name,
|
|
1159
|
-
"primary_directive": npc.primary_directive,
|
|
1160
|
-
"model": npc.model,
|
|
1161
|
-
"provider": npc.provider,
|
|
1162
|
-
"api_url": npc.api_url,
|
|
1163
|
-
"use_global_jinxs": npc.use_global_jinxs,
|
|
1164
|
-
"jinxs": [
|
|
1165
|
-
{
|
|
1166
|
-
"jinx_name": jinx.jinx_name,
|
|
1167
|
-
"inputs": jinx.inputs,
|
|
1168
|
-
"steps": [
|
|
1169
|
-
{
|
|
1170
|
-
"name": step.get("name", f"step_{i}"),
|
|
1171
|
-
"engine": step.get("engine", "natural"),
|
|
1172
|
-
"code": step.get("code", "")
|
|
1173
|
-
}
|
|
1174
|
-
for i, step in enumerate(jinx.steps)
|
|
1175
|
-
]
|
|
1176
|
-
}
|
|
1177
|
-
for jinx in npc.jinxs
|
|
1178
|
-
],
|
|
1179
|
-
}
|
|
1180
|
-
npc_data.append(serialized_npc)
|
|
1181
1217
|
|
|
1182
|
-
print(npc_data)
|
|
1218
|
+
print(f"Project NPC data: {npc_data}", file=sys.stderr) # Diagnostic print
|
|
1183
1219
|
return jsonify({"npcs": npc_data, "error": None})
|
|
1184
1220
|
|
|
1185
1221
|
except Exception as e:
|
|
1186
|
-
print(f"Error fetching NPC team: {str(e)}")
|
|
1222
|
+
print(f"Error fetching NPC team: {str(e)}", file=sys.stderr)
|
|
1223
|
+
traceback.print_exc(file=sys.stderr)
|
|
1187
1224
|
return jsonify({"npcs": [], "error": str(e)})
|
|
1225
|
+
|
|
1226
|
+
|
|
1188
1227
|
def get_last_used_model_and_npc_in_directory(directory_path):
|
|
1189
1228
|
"""
|
|
1190
1229
|
Fetches the model and NPC from the most recent message in any conversation
|
|
@@ -1985,7 +2024,8 @@ def stream():
|
|
|
1985
2024
|
npc_name = data.get("npc", None)
|
|
1986
2025
|
npc_source = data.get("npcSource", "global")
|
|
1987
2026
|
current_path = data.get("currentPath")
|
|
1988
|
-
|
|
2027
|
+
is_resend = data.get("isResend", False) # ADD THIS LINE
|
|
2028
|
+
|
|
1989
2029
|
if current_path:
|
|
1990
2030
|
loaded_vars = load_project_env(current_path)
|
|
1991
2031
|
print(f"Loaded project env variables for stream request: {list(loaded_vars.keys())}")
|
|
@@ -2321,20 +2361,25 @@ def stream():
|
|
|
2321
2361
|
for cont in messages[-1].get('content'):
|
|
2322
2362
|
txt = cont.get('text')
|
|
2323
2363
|
if txt is not None:
|
|
2324
|
-
user_message_filled +=txt
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2364
|
+
user_message_filled += txt
|
|
2365
|
+
|
|
2366
|
+
# Only save user message if it's NOT a resend
|
|
2367
|
+
if not is_resend: # ADD THIS CONDITION
|
|
2368
|
+
save_conversation_message(
|
|
2369
|
+
command_history,
|
|
2370
|
+
conversation_id,
|
|
2371
|
+
"user",
|
|
2372
|
+
user_message_filled if len(user_message_filled) > 0 else commandstr,
|
|
2373
|
+
wd=current_path,
|
|
2374
|
+
model=model,
|
|
2375
|
+
provider=provider,
|
|
2376
|
+
npc=npc_name,
|
|
2377
|
+
team=team,
|
|
2378
|
+
attachments=attachments_for_db,
|
|
2379
|
+
message_id=message_id,
|
|
2380
|
+
)
|
|
2381
|
+
|
|
2382
|
+
|
|
2338
2383
|
|
|
2339
2384
|
|
|
2340
2385
|
message_id = generate_message_id()
|
|
@@ -2478,7 +2523,33 @@ def stream():
|
|
|
2478
2523
|
|
|
2479
2524
|
return Response(event_stream(stream_id), mimetype="text/event-stream")
|
|
2480
2525
|
|
|
2481
|
-
|
|
2526
|
+
@app.route('/api/delete_message', methods=['POST'])
|
|
2527
|
+
def delete_message():
|
|
2528
|
+
data = request.json
|
|
2529
|
+
conversation_id = data.get('conversationId')
|
|
2530
|
+
message_id = data.get('messageId')
|
|
2531
|
+
|
|
2532
|
+
if not conversation_id or not message_id:
|
|
2533
|
+
return jsonify({"error": "Missing conversationId or messageId"}), 400
|
|
2534
|
+
|
|
2535
|
+
try:
|
|
2536
|
+
command_history = CommandHistory(app.config.get('DB_PATH'))
|
|
2537
|
+
|
|
2538
|
+
# Delete the message from the database
|
|
2539
|
+
result = command_history.delete_message(conversation_id, message_id)
|
|
2540
|
+
|
|
2541
|
+
print(f"[DELETE_MESSAGE] Deleted message {message_id} from conversation {conversation_id}. Rows affected: {result}")
|
|
2542
|
+
|
|
2543
|
+
return jsonify({
|
|
2544
|
+
"success": True,
|
|
2545
|
+
"deletedMessageId": message_id,
|
|
2546
|
+
"rowsAffected": result
|
|
2547
|
+
}), 200
|
|
2548
|
+
|
|
2549
|
+
except Exception as e:
|
|
2550
|
+
print(f"[DELETE_MESSAGE] Error: {e}")
|
|
2551
|
+
traceback.print_exc()
|
|
2552
|
+
return jsonify({"error": str(e)}), 500
|
|
2482
2553
|
|
|
2483
2554
|
@app.route("/api/memory/approve", methods=["POST"])
|
|
2484
2555
|
def approve_memories():
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
npcpy/__init__.py,sha256=9imxFtK74_6Rw9rz0kyMnZYl_voPb569tkTlYLt0Urg,131
|
|
2
2
|
npcpy/llm_funcs.py,sha256=RtZAtX1_nvfn-X3IHVyQggDBXDzGxKEix_sS_iliNN0,87172
|
|
3
3
|
npcpy/main.py,sha256=RWoRIj6VQLxKdOKvdVyaq2kwG35oRpeXPvp1CAAoG-w,81
|
|
4
|
-
npcpy/npc_compiler.py,sha256=
|
|
4
|
+
npcpy/npc_compiler.py,sha256=p__rbudWTbhOKJyHxaalP0I33HEfohWfwqQ9Q5q4ROY,99091
|
|
5
5
|
npcpy/npc_sysenv.py,sha256=t9AswM-9_P2NaGsnlzTMc2hUfdSthi9ofbud6F1G7LM,35974
|
|
6
6
|
npcpy/npcs.py,sha256=eExuVsbTfrRobTRRptRpDm46jCLWUgbvy4_U7IUQo-c,744
|
|
7
|
-
npcpy/serve.py,sha256=
|
|
7
|
+
npcpy/serve.py,sha256=555kHrKEsQGVfJpisdShIDxekb39Z7X86z2bt5EdqRM,121575
|
|
8
8
|
npcpy/tools.py,sha256=A5_oVmZkzGnI3BI-NmneuxeXQq-r29PbpAZP4nV4jrc,5303
|
|
9
9
|
npcpy/data/__init__.py,sha256=1tcoChR-Hjn905JDLqaW9ElRmcISCTJdE7BGXPlym2Q,642
|
|
10
10
|
npcpy/data/audio.py,sha256=goon4HfsYgx0bI-n1lhkrzWPrJoejJlycXcB0P62pyk,11280
|
|
@@ -29,7 +29,7 @@ npcpy/gen/image_gen.py,sha256=mAlLG9jo9RnuuMU0jJVV0CpIgHqdizU9sfC6A0w5kKE,15599
|
|
|
29
29
|
npcpy/gen/response.py,sha256=6iAOi4hxUxkTZ1d2suBUASOssT6pQnr3HFwZWrvmATg,31925
|
|
30
30
|
npcpy/gen/video_gen.py,sha256=RFi3Zcq_Hn3HIcfoF3mijQ6G7RYFZaM_9pjPTh-8E64,3239
|
|
31
31
|
npcpy/memory/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
32
|
-
npcpy/memory/command_history.py,sha256=
|
|
32
|
+
npcpy/memory/command_history.py,sha256=xdDu1LNPtdKRUX-kcOz7fL5XoimGu76HBeTQLTiMQ7w,47173
|
|
33
33
|
npcpy/memory/kg_vis.py,sha256=TrQQCRh_E7Pyr-GPAHLSsayubAfGyf4HOEFrPB6W86Q,31280
|
|
34
34
|
npcpy/memory/knowledge_graph.py,sha256=2XpIlsyPdAOnzQ6kkwP6MWPGwL3P6V33_3suNJYMMJE,48681
|
|
35
35
|
npcpy/memory/memory_processor.py,sha256=6PfVnSBA9ag5EhHJinXoODfEPTlDDoaT0PtCCuZO6HI,2598
|
|
@@ -47,8 +47,8 @@ npcpy/work/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
47
47
|
npcpy/work/desktop.py,sha256=F3I8mUtJp6LAkXodsh8hGZIncoads6c_2Utty-0EdDA,2986
|
|
48
48
|
npcpy/work/plan.py,sha256=QyUwg8vElWiHuoS-xK4jXTxxHvkMD3VkaCEsCmrEPQk,8300
|
|
49
49
|
npcpy/work/trigger.py,sha256=P1Y8u1wQRsS2WACims_2IdkBEar-iBQix-2TDWoW0OM,9948
|
|
50
|
-
npcpy-1.2.
|
|
51
|
-
npcpy-1.2.
|
|
52
|
-
npcpy-1.2.
|
|
53
|
-
npcpy-1.2.
|
|
54
|
-
npcpy-1.2.
|
|
50
|
+
npcpy-1.2.34.dist-info/licenses/LICENSE,sha256=j0YPvce7Ng9e32zYOu0EmXjXeJ0Nwawd0RA3uSGGH4E,1070
|
|
51
|
+
npcpy-1.2.34.dist-info/METADATA,sha256=tEmBrassMmZLHnvB4zXS0Cr8qj0Of-wFfLkdUjFqy2Y,33537
|
|
52
|
+
npcpy-1.2.34.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
53
|
+
npcpy-1.2.34.dist-info/top_level.txt,sha256=g1pbSvrOOncB74Bg5-J0Olg4V0A5VzDw-Xz5YObq8BU,6
|
|
54
|
+
npcpy-1.2.34.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|