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.
@@ -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
- # Store the raw steps as loaded from YAML
256
- self._raw_steps = list(self.steps) # Make a copy to preserve original
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", []) # These are the raw steps initially
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): # Add jinja_env here for the second pass
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 = (active_npc.shared_context.copy() if active_npc and hasattr(active_npc, 'shared_context') else {})
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, # This is the second-pass 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, # This is for the second pass (runtime vars)
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
- code_content = step.get("code", "") # Get the code content, which might still have {{ runtime_var }}
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
- print(f"Error rendering template for step {step_name} (second pass): {e}")
440
- rendered_code = code_content # Fallback to unrendered if error
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 = f"Error executing step {step_name}: {type(e).__name__}: {e}"
477
+ error_msg = (
478
+ f"Error executing step {step_name}: "
479
+ f"{type(e).__name__}: {e}"
480
+ )
469
481
  context['output'] = error_msg
470
- print(error_msg)
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': f'Jinx {self.jinx_name} step {step_name} executed: {outp}'
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 = param.annotation if param.annotation != inspect.Parameter.empty else None
521
- param_default = None if param.default == inspect.Parameter.empty else param.default
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([f'{inp["name"]}=context.get("{inp["name"]}")' for inp in inputs])}
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("/api/jinxs/global", methods=["GET"])
553
+ @app.route('/api/jinxs/global', methods=['GET'])
539
554
  def get_global_jinxs():
540
- jinxs_dir = os.path.join(os.path.expanduser("~"), ".npcsh", "npc_team", "jinxs")
541
- jinx_paths = _get_jinx_files_recursively(jinxs_dir)
542
- jinxs = []
543
- for path in jinx_paths:
544
- try:
545
- with open(path, "r") as f:
546
- jinx_data = yaml.safe_load(f)
547
- jinxs.append(jinx_data)
548
- except Exception as e:
549
- print(f"Error loading global jinx {path}: {e}")
550
- return jsonify({"jinxs": jinxs})
551
-
552
- @app.route("/api/jinxs/project", methods=["GET"])
553
- def get_project_jinxs():
554
- current_path = request.args.get("currentPath")
555
- if not current_path:
556
- return jsonify({"jinxs": []})
557
-
558
- if not current_path.endswith("npc_team"):
559
- current_path = os.path.join(current_path, "npc_team")
560
-
561
- jinxs_dir = os.path.join(current_path, "jinxs")
562
- jinx_paths = _get_jinx_files_recursively(jinxs_dir)
563
- jinxs = []
564
- for path in jinx_paths:
565
- try:
566
- with open(path, "r") as f:
567
- jinx_data = yaml.safe_load(f)
568
- jinxs.append(jinx_data)
569
- except Exception as e:
570
- print(f"Error loading project jinx {path}: {e}")
571
- return jsonify({"jinxs": jinxs})
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
- Streams the output back to the client.
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
- # Load the NPC
616
- npc_object = None
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
- team_jinx_path = os.path.join(current_path, 'npc_team', 'jinxs', f'{jinx_name}.jinx')
633
- if os.path.exists(team_jinx_path):
634
- jinx = Jinx(jinx_path=team_jinx_path)
635
-
636
- # Check global jinxs
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
- global_jinx_path = os.path.expanduser(f'~/.npcsh/npc_team/jinxs/{jinx_name}.jinx')
639
- if os.path.exists(global_jinx_path):
640
- jinx = Jinx(jinx_path=global_jinx_path)
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
- while i < len(jinx_args):
653
- arg = jinx_args[i]
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
- # Collect all subsequent parts until the next flag or the end of the list.
659
- while i < len(jinx_args) and not jinx_args[i].startswith('-'):
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
- # Get conversation history
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
- def event_stream(current_stream_id):
700
- try:
701
- # --- IMPORTANT: Pass the persistent context as 'extra_globals' ---
702
- result = jinx.execute(
703
- input_values=input_values,
704
- jinxs_dict=all_jinxs,
705
- jinja_env=npc_object.jinja_env if npc_object else None,
706
- npc=npc_object,
707
- messages=messages,
708
- extra_globals=jinx_local_context # <--- THIS IS WHERE THE PERSISTENT CONTEXT IS PASSED
709
- )
710
-
711
- # --- CRITICAL FIX: Capture and update local_vars from the Jinx's result ---
712
- # The Jinx.execute method returns its internal 'context' dictionary.
713
- # We need to update our persistent 'jinx_local_context' with the new variables
714
- # from the Jinx's returned context.
715
- if isinstance(result, dict):
716
- # We need to be careful not to overwrite core Jinx/NPC context keys
717
- # that are not meant for variable persistence.
718
- keys_to_exclude = ['output', 'llm_response', 'messages', 'results', 'npc', 'context', 'jinxs', 'team']
719
-
720
- # Update jinx_local_context with all non-excluded keys from the result
721
- for key, value in result.items():
722
- if key not in keys_to_exclude and not key.startswith('_'): # Exclude internal/temporary keys
723
- jinx_local_context[key] = value
724
-
725
- print(f"jinx_local_context UPDATED from Jinx result: {jinx_local_context}") # NEW LOG
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
- print(f"jinx_local_context AFTER Jinx execution (final state): {jinx_local_context}")
732
- print(f"Jinx execution result output: {output}")
733
-
734
- # Check for interruption
735
- with cancellation_lock:
736
- if cancellation_flags.get(current_stream_id, False):
737
- yield f"data: {json.dumps({'type': 'interrupted'})}\n\n"
738
- return
739
-
740
- # Stream the output in chunks for consistent UI experience
741
- if isinstance(output, str):
742
- chunk_size = 50 # Characters per chunk
743
- for i in range(0, len(output), chunk_size):
744
- chunk = output[i:i + chunk_size]
745
- chunk_data = {
746
- "id": None,
747
- "object": None,
748
- "created": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
749
- "model": model,
750
- "choices": [{
751
- "index": 0,
752
- "delta": {
753
- "content": chunk,
754
- "role": "assistant"
755
- },
756
- "finish_reason": None
757
- }]
758
- }
759
- yield f"data: {json.dumps(chunk_data)}\n\n"
760
- else:
761
- # Non-string output, send as single chunk
762
- chunk_data = {
763
- "id": None,
764
- "object": None,
765
- "created": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
766
- "model": model,
767
- "choices": [{
768
- "index": 0,
769
- "delta": {
770
- "content": str(output),
771
- "role": "assistant"
772
- },
773
- "finish_reason": None
774
- }]
775
- }
776
- yield f"data: {json.dumps(chunk_data)}\n\n"
777
-
778
- # Send completion message
779
- yield f"data: {json.dumps({'type': 'message_stop'})}\n\n"
780
-
781
- # Save to conversation history
782
- message_id = generate_message_id()
783
- save_conversation_message(
784
- command_history,
785
- conversation_id,
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
- npc = NPC(file=npc_path, db_conn=db_conn)
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
- save_conversation_message(
2326
- command_history,
2327
- conversation_id,
2328
- "user",
2329
- user_message_filled if len(user_message_filled)>0 else commandstr,
2330
- wd=current_path,
2331
- model=model,
2332
- provider=provider,
2333
- npc=npc_name,
2334
- team=team,
2335
- attachments=attachments_for_db,
2336
- message_id=message_id,
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: npcpy
3
- Version: 1.2.33
3
+ Version: 1.2.34
4
4
  Summary: npcpy is the premier open-source library for integrating LLMs and Agents into python systems.
5
5
  Home-page: https://github.com/NPC-Worldwide/npcpy
6
6
  Author: Christopher Agostino
@@ -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=rO4ws2ph9s3WmM0MDOfKXwp6bD5eP18XjQ0j1dtntoQ,102738
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=P01tYsY1ctq408nn-t3sLPGuGJg5KoaApy4gNECDRgo,118007
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=2VdmNW5VRpMrOkbdrMsgn5p3mvuJHNnzGHnIUEM8XMI,46279
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.33.dist-info/licenses/LICENSE,sha256=j0YPvce7Ng9e32zYOu0EmXjXeJ0Nwawd0RA3uSGGH4E,1070
51
- npcpy-1.2.33.dist-info/METADATA,sha256=er9nYm5vqeGZEyetapLVAuPci4TIfwdmODV_NRX_RWI,33537
52
- npcpy-1.2.33.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
53
- npcpy-1.2.33.dist-info/top_level.txt,sha256=g1pbSvrOOncB74Bg5-J0Olg4V0A5VzDw-Xz5YObq8BU,6
54
- npcpy-1.2.33.dist-info/RECORD,,
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