npcpy 1.2.29__py3-none-any.whl → 1.2.30__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/llm_funcs.py CHANGED
@@ -378,7 +378,6 @@ def execute_llm_command(
378
378
  "messages": messages,
379
379
  "output": "Max attempts reached. Unable to execute the command successfully.",
380
380
  }
381
-
382
381
  def handle_jinx_call(
383
382
  command: str,
384
383
  jinx_name: str,
@@ -391,6 +390,7 @@ def handle_jinx_call(
391
390
  n_attempts=3,
392
391
  attempt=0,
393
392
  context=None,
393
+ extra_globals=None, # ADD THIS
394
394
  **kwargs
395
395
  ) -> Union[str, Dict[str, Any]]:
396
396
  """This function handles a jinx call.
@@ -568,6 +568,8 @@ def handle_jinx_call(
568
568
  jinja_env,
569
569
  npc=npc,
570
570
  messages=messages,
571
+ extra_globals=extra_globals # ADD THIS
572
+
571
573
  )
572
574
  except Exception as e:
573
575
  print(f"An error occurred while executing the jinx: {e}")
@@ -664,10 +666,10 @@ def jinx_handler(command, extracted_data, **kwargs):
664
666
  api_key=kwargs.get('api_key'),
665
667
  messages=kwargs.get('messages'),
666
668
  npc=kwargs.get('npc'),
667
- team = kwargs.get('team'),
669
+ team=kwargs.get('team'),
668
670
  stream=kwargs.get('stream'),
669
-
670
- context=kwargs.get('context')
671
+ context=kwargs.get('context'),
672
+ extra_globals=kwargs.get('extra_globals') # ADD THIS
671
673
  )
672
674
 
673
675
  def answer_handler(command, extracted_data, **kwargs):
@@ -714,6 +716,7 @@ def check_llm_command(
714
716
  stream=False,
715
717
  context=None,
716
718
  actions: Dict[str, Dict] = None,
719
+ extra_globals=None,
717
720
  ):
718
721
  """This function checks an LLM command and returns sequences of steps with parallel actions."""
719
722
  if messages is None:
@@ -734,6 +737,7 @@ def check_llm_command(
734
737
  stream=stream,
735
738
  context=context,
736
739
  actions=actions,
740
+ extra_globals=extra_globals,
737
741
 
738
742
  )
739
743
  return exec
@@ -873,6 +877,7 @@ def plan_multi_step_actions(
873
877
  api_key: str = None,
874
878
  context: str = None,
875
879
  messages: List[Dict[str, str]] = None,
880
+
876
881
 
877
882
  ):
878
883
  """
@@ -992,6 +997,7 @@ def execute_multi_step_plan(
992
997
  messages=messages,
993
998
  team=team,
994
999
 
1000
+
995
1001
  )
996
1002
 
997
1003
  if not planned_actions:
@@ -1007,7 +1013,8 @@ def execute_multi_step_plan(
1007
1013
  stream=stream,
1008
1014
  team = team,
1009
1015
  images=images,
1010
- context=context)
1016
+ context=context
1017
+ )
1011
1018
  return {"messages": result.get('messages',
1012
1019
  messages),
1013
1020
  "output": result.get('response')}
@@ -1037,7 +1044,7 @@ def execute_multi_step_plan(
1037
1044
  render_markdown(
1038
1045
  f"- Executing Action: {action_name} \n- Explanation: {action_data.get('explanation')}\n "
1039
1046
  )
1040
-
1047
+
1041
1048
  result = handler(
1042
1049
  command=command,
1043
1050
  extracted_data=action_data,
@@ -1049,10 +1056,10 @@ def execute_multi_step_plan(
1049
1056
  npc=npc,
1050
1057
  team=team,
1051
1058
  stream=stream,
1052
-
1053
1059
  context=context+step_context,
1054
- images=images
1055
- )
1060
+ images=images,
1061
+ extra_globals=kwargs.get('extra_globals') # ADD THIS
1062
+ )
1056
1063
  except KeyError as e:
1057
1064
 
1058
1065
  return execute_multi_step_plan(
@@ -1068,6 +1075,7 @@ def execute_multi_step_plan(
1068
1075
  stream=stream,
1069
1076
  context=context,
1070
1077
  actions=actions,
1078
+
1071
1079
  **kwargs,
1072
1080
  )
1073
1081
 
npcpy/npc_compiler.py CHANGED
@@ -280,50 +280,59 @@ class Jinx:
280
280
  else:
281
281
  raise ValueError(f"Invalid step format: {step}")
282
282
  return parsed_steps
283
+
283
284
  def execute(self,
284
- input_values,
285
- jinxs_dict,
286
- jinja_env = None,
287
- npc = None,
288
- messages=None):
289
- """Execute the jinx with given inputs"""
285
+ input_values: Dict[str, Any],
286
+ jinxs_dict: Dict[str, 'Jinx'],
287
+ jinja_env: Optional[Environment] = None,
288
+ npc: Optional[Any] = None,
289
+ messages: Optional[List[Dict[str, str]]] = None,
290
+ **kwargs: Any):
291
+ """
292
+ Execute the jinx with given inputs.
293
+ **kwargs can be used to pass 'extra_globals' for the python engine.
294
+ """
290
295
  if jinja_env is None:
291
-
292
-
293
296
  from jinja2 import DictLoader
294
297
  jinja_env = Environment(
295
- loader=DictLoader({}),
298
+ loader=DictLoader({}),
296
299
  undefined=SilentUndefined,
297
300
  )
298
301
 
299
- context = (npc.shared_context.copy() if npc else {})
302
+ context = (npc.shared_context.copy() if npc and hasattr(npc, 'shared_context') else {})
300
303
  context.update(input_values)
301
304
  context.update({
302
305
  "jinxs": jinxs_dict,
303
306
  "llm_response": None,
304
- "output": None,
307
+ "output": None,
305
308
  "messages": messages,
306
309
  })
307
310
 
308
-
311
+ # This is the key change: Extract 'extra_globals' from kwargs
312
+ extra_globals = kwargs.get('extra_globals')
313
+
309
314
  for i, step in enumerate(self.steps):
310
315
  context = self._execute_step(
311
- step,
316
+ step,
312
317
  context,
313
- jinja_env,
314
- npc=npc,
315
- messages=messages,
316
-
317
- )
318
+ jinja_env,
319
+ npc=npc,
320
+ messages=messages,
321
+ extra_globals=extra_globals # Pass it down to the step executor
322
+ )
318
323
 
319
324
  return context
325
+
320
326
  def _execute_step(self,
321
- step,
322
- context,
323
- jinja_env,
324
- npc=None,
325
- messages=None,
326
- ):
327
+ step: Dict[str, Any],
328
+ context: Dict[str, Any],
329
+ jinja_env: Environment,
330
+ npc: Optional[Any] = None,
331
+ messages: Optional[List[Dict[str, str]]] = None,
332
+ extra_globals: Optional[Dict[str, Any]] = None):
333
+ """
334
+ Execute a single step of the jinx.
335
+ """
327
336
  engine = step.get("engine", "natural")
328
337
  code = step.get("code", "")
329
338
  step_name = step.get("name", "unnamed_step")
@@ -364,7 +373,9 @@ class Jinx:
364
373
  context["results"] = response_text
365
374
  context[step_name] = response_text
366
375
  context['messages'] = response.get('messages')
376
+
367
377
  elif rendered_engine == "python":
378
+ # Base globals available to all python jinxes, defined within the library (npcpy)
368
379
  exec_globals = {
369
380
  "__builtins__": __builtins__,
370
381
  "npc": npc,
@@ -379,21 +390,33 @@ class Jinx:
379
390
  "fnmatch": fnmatch,
380
391
  "pathlib": pathlib,
381
392
  "subprocess": subprocess,
382
- "get_llm_response": npy.llm_funcs.get_llm_response,
383
- }
393
+ "get_llm_response": npy.llm_funcs.get_llm_response,
394
+ "CommandHistory": CommandHistory, # This is fine, it's part of npcpy
395
+ }
384
396
 
385
- exec_locals = {}
386
- exec(rendered_code, exec_globals, exec_locals)
397
+ # This is the fix: Update the globals with the dictionary passed in from the application (npcsh)
398
+ if extra_globals:
399
+ exec_globals.update(extra_globals)
387
400
 
401
+ exec_locals = {}
402
+ try:
403
+ exec(rendered_code, exec_globals, exec_locals)
404
+ except Exception as e:
405
+ # Provide a clear error message in the output if execution fails
406
+ error_msg = f"Error executing jinx python code: {type(e).__name__}: {e}"
407
+ context['output'] = error_msg
408
+ return context
409
+
388
410
  context.update(exec_locals)
389
411
 
390
412
  if "output" in exec_locals:
391
413
  outp = exec_locals["output"]
392
414
  context["output"] = outp
393
415
  context[step_name] = outp
394
- messages.append({'role':'assistant',
395
- 'content': f'Jinx executed with following output: {outp}'})
396
- context['messages'] = messages
416
+ if messages is not None:
417
+ messages.append({'role':'assistant',
418
+ 'content': f'Jinx executed with following output: {outp}'})
419
+ context['messages'] = messages
397
420
 
398
421
  else:
399
422
  context[step_name] = {"error": f"Unsupported engine: {rendered_engine}"}
@@ -561,6 +584,9 @@ def get_npc_action_space(npc=None, team=None):
561
584
 
562
585
  return actions
563
586
  def extract_jinx_inputs(args: List[str], jinx: Jinx) -> Dict[str, Any]:
587
+ print(f"DEBUG extract_jinx_inputs called with args: {args}")
588
+ print(f"DEBUG jinx.inputs: {jinx.inputs}")
589
+
564
590
  inputs = {}
565
591
 
566
592
  flag_mapping = {}
@@ -585,7 +611,6 @@ def extract_jinx_inputs(args: List[str], jinx: Jinx) -> Dict[str, Any]:
585
611
  else:
586
612
  used_args = set()
587
613
 
588
-
589
614
  for i, arg in enumerate(args):
590
615
  if i in used_args:
591
616
  continue
@@ -603,21 +628,38 @@ def extract_jinx_inputs(args: List[str], jinx: Jinx) -> Dict[str, Any]:
603
628
 
604
629
  unused_args = [arg for i, arg in enumerate(args) if i not in used_args]
605
630
 
606
- jinx_input_names = []
631
+ print(f"DEBUG unused_args: {unused_args}")
632
+
633
+ # Find first required input (no default value)
634
+ first_required = None
607
635
  for input_ in jinx.inputs:
608
636
  if isinstance(input_, str):
609
- jinx_input_names.append(input_)
610
- elif isinstance(input_, dict):
611
- jinx_input_names.append(list(input_.keys())[0])
612
- if len(jinx_input_names) == 1:
613
- inputs[jinx_input_names[0]] = ' '.join(unused_args).strip()
637
+ first_required = input_
638
+ break
639
+
640
+ print(f"DEBUG first_required: {first_required}")
641
+
642
+ # Give all unused args to first required input
643
+ if first_required and unused_args:
644
+ inputs[first_required] = ' '.join(unused_args).strip()
645
+ print(f"DEBUG assigned to first_required: {inputs[first_required]}")
614
646
  else:
615
- for i, arg in enumerate(unused_args):
616
- if i < len(jinx_input_names):
617
- input_name = jinx_input_names[i]
618
- if input_name not in inputs:
619
- inputs[input_name] = arg
620
-
647
+ # Fallback to original behavior
648
+ jinx_input_names = []
649
+ for input_ in jinx.inputs:
650
+ if isinstance(input_, str):
651
+ jinx_input_names.append(input_)
652
+ elif isinstance(input_, dict):
653
+ jinx_input_names.append(list(input_.keys())[0])
654
+
655
+ if len(jinx_input_names) == 1:
656
+ inputs[jinx_input_names[0]] = ' '.join(unused_args).strip()
657
+ else:
658
+ for i, arg in enumerate(unused_args):
659
+ if i < len(jinx_input_names):
660
+ input_name = jinx_input_names[i]
661
+ if input_name not in inputs:
662
+ inputs[input_name] = arg
621
663
 
622
664
  for input_ in jinx.inputs:
623
665
  if isinstance(input_, str):
@@ -629,8 +671,8 @@ def extract_jinx_inputs(args: List[str], jinx: Jinx) -> Dict[str, Any]:
629
671
  if key not in inputs:
630
672
  inputs[key] = default_value
631
673
 
674
+ print(f"DEBUG final inputs: {inputs}")
632
675
  return inputs
633
-
634
676
  from npcpy.memory.command_history import load_kg_from_db, save_kg_to_db
635
677
  from npcpy.memory.knowledge_graph import kg_initial, kg_evolve_incremental, kg_sleep_process, kg_dream_process
636
678
  from npcpy.llm_funcs import get_llm_response, breathe
npcpy/serve.py CHANGED
@@ -149,7 +149,7 @@ def load_kg_data(generation=None):
149
149
  app = Flask(__name__)
150
150
  app.config["REDIS_URL"] = "redis://localhost:6379"
151
151
  app.config['DB_PATH'] = ''
152
-
152
+ app.jinx_conversation_contexts ={}
153
153
 
154
154
  redis_client = redis.Redis(host="localhost", port=6379, decode_responses=True)
155
155
 
@@ -585,19 +585,27 @@ def execute_jinx():
585
585
  with cancellation_lock:
586
586
  cancellation_flags[stream_id] = False
587
587
 
588
- print(data)
588
+ print(f"--- Jinx Execution Request for streamId: {stream_id} ---")
589
+ print(f"Request Data: {json.dumps(data, indent=2)}")
589
590
 
590
591
  jinx_name = data.get("jinxName")
591
592
  jinx_args = data.get("jinxArgs", [])
592
- print(jinx_args)
593
+ print(f"Jinx Name: {jinx_name}, Jinx Args: {jinx_args}")
593
594
  conversation_id = data.get("conversationId")
594
595
  model = data.get("model")
595
596
  provider = data.get("provider")
597
+
598
+ # --- IMPORTANT: Ensure conversation_id is present for context persistence ---
599
+ if not conversation_id:
600
+ print("ERROR: conversationId is required for Jinx execution with persistent variables")
601
+ return jsonify({"error": "conversationId is required for Jinx execution with persistent variables"}), 400
602
+
596
603
  npc_name = data.get("npc")
597
604
  npc_source = data.get("npcSource", "global")
598
605
  current_path = data.get("currentPath")
599
606
 
600
607
  if not jinx_name:
608
+ print("ERROR: jinxName is required")
601
609
  return jsonify({"error": "jinxName is required"}), 400
602
610
 
603
611
  # Load project environment if applicable
@@ -632,12 +640,12 @@ def execute_jinx():
632
640
  jinx = Jinx(jinx_path=global_jinx_path)
633
641
 
634
642
  if not jinx:
643
+ print(f"ERROR: Jinx '{jinx_name}' not found")
635
644
  return jsonify({"error": f"Jinx '{jinx_name}' not found"}), 404
636
645
 
637
646
  # Extract inputs from args
638
647
  from npcpy.npc_compiler import extract_jinx_inputs
639
648
 
640
- # --- Start of Fix ---
641
649
  # Re-assemble arguments that were incorrectly split by spaces.
642
650
  fixed_args = []
643
651
  i = 0
@@ -666,15 +674,11 @@ def execute_jinx():
666
674
  # This handles positional arguments, just in case.
667
675
  fixed_args.append(arg)
668
676
  i += 1
669
- # --- End of Fix ---
670
677
 
671
678
  # Now, use the corrected arguments to extract inputs.
672
679
  input_values = extract_jinx_inputs(fixed_args, jinx)
673
680
 
674
-
675
-
676
-
677
- print('executing jinx with input_values ,', input_values)
681
+ print(f'Executing jinx with input_values: {input_values}')
678
682
  # Get conversation history
679
683
  command_history = CommandHistory(app.config.get('DB_PATH'))
680
684
  messages = fetch_messages_for_conversation(conversation_id)
@@ -684,20 +688,48 @@ def execute_jinx():
684
688
  if npc_object and hasattr(npc_object, 'jinxs_dict'):
685
689
  all_jinxs.update(npc_object.jinxs_dict)
686
690
 
691
+ # --- IMPORTANT: Retrieve or initialize the persistent Jinx context for this conversation ---
692
+ if conversation_id not in app.jinx_conversation_contexts:
693
+ app.jinx_conversation_contexts[conversation_id] = {}
694
+ jinx_local_context = app.jinx_conversation_contexts[conversation_id]
695
+
696
+ print(f"--- CONTEXT STATE (conversationId: {conversation_id}) ---")
697
+ print(f"jinx_local_context BEFORE Jinx execution: {jinx_local_context}")
698
+
687
699
  def event_stream(current_stream_id):
688
700
  try:
689
- # Execute the jinx
701
+ # --- IMPORTANT: Pass the persistent context as 'extra_globals' ---
690
702
  result = jinx.execute(
691
703
  input_values=input_values,
692
704
  jinxs_dict=all_jinxs,
693
705
  jinja_env=npc_object.jinja_env if npc_object else None,
694
706
  npc=npc_object,
695
- messages=messages
707
+ messages=messages,
708
+ extra_globals=jinx_local_context # <--- THIS IS WHERE THE PERSISTENT CONTEXT IS PASSED
696
709
  )
697
710
 
698
- # Get output
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)
699
728
  output = result.get('output', str(result))
700
729
  messages_updated = result.get('messages', messages)
730
+
731
+ print(f"jinx_local_context AFTER Jinx execution (final state): {jinx_local_context}")
732
+ print(f"Jinx execution result output: {output}")
701
733
 
702
734
  # Check for interruption
703
735
  with cancellation_lock:
@@ -774,7 +806,7 @@ def execute_jinx():
774
806
  )
775
807
 
776
808
  except Exception as e:
777
- print(f"Error executing jinx {jinx_name}: {str(e)}")
809
+ print(f"ERROR: Exception during jinx execution {jinx_name}: {str(e)}")
778
810
  traceback.print_exc()
779
811
  error_data = {
780
812
  "type": "error",
@@ -786,6 +818,7 @@ def execute_jinx():
786
818
  with cancellation_lock:
787
819
  if current_stream_id in cancellation_flags:
788
820
  del cancellation_flags[current_stream_id]
821
+ print(f"--- Jinx Execution Finished for streamId: {stream_id} ---")
789
822
 
790
823
  return Response(event_stream(stream_id), mimetype="text/event-stream")
791
824
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: npcpy
3
- Version: 1.2.29
3
+ Version: 1.2.30
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
- npcpy/llm_funcs.py,sha256=UkesCnRmclEoqBZPMZa2hKoSTjFzjxDCzPGKgeDegPQ,85101
2
+ npcpy/llm_funcs.py,sha256=80vg6sEH3oGNPrxArQOVyNyfmvjsmS7xHuCFT540RkU,85499
3
3
  npcpy/main.py,sha256=RWoRIj6VQLxKdOKvdVyaq2kwG35oRpeXPvp1CAAoG-w,81
4
- npcpy/npc_compiler.py,sha256=hXnUV9idoqZeNMiv2U1TNQxBNucvMXfF3HZqNbZB_8E,87276
4
+ npcpy/npc_compiler.py,sha256=j3JYZPKPLi42HAEA_i3Cp5GBGGUcpzBk8OEzZEvxzY4,89458
5
5
  npcpy/npc_sysenv.py,sha256=QSLkkAsCeSiyE525yWMZCBwm5_UB8--A8O-8vgx5unY,35218
6
6
  npcpy/npcs.py,sha256=eExuVsbTfrRobTRRptRpDm46jCLWUgbvy4_U7IUQo-c,744
7
- npcpy/serve.py,sha256=htmVw2o5WiCOgUgkrIs90dnszBqwKPKnPLQlAgG_Uds,115397
7
+ npcpy/serve.py,sha256=PQxhe152grD5QhsQyqWV71TX9Bim4P5ykjhPLtHvqYA,117933
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
@@ -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.29.dist-info/licenses/LICENSE,sha256=j0YPvce7Ng9e32zYOu0EmXjXeJ0Nwawd0RA3uSGGH4E,1070
51
- npcpy-1.2.29.dist-info/METADATA,sha256=d05EPbUMmAl-KBXuMDKETwbdDPVjkaoogOscOodeOe0,29895
52
- npcpy-1.2.29.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
53
- npcpy-1.2.29.dist-info/top_level.txt,sha256=g1pbSvrOOncB74Bg5-J0Olg4V0A5VzDw-Xz5YObq8BU,6
54
- npcpy-1.2.29.dist-info/RECORD,,
50
+ npcpy-1.2.30.dist-info/licenses/LICENSE,sha256=j0YPvce7Ng9e32zYOu0EmXjXeJ0Nwawd0RA3uSGGH4E,1070
51
+ npcpy-1.2.30.dist-info/METADATA,sha256=F1LvFSLkzAAFwYIj3ZRGyhsvblq9Izef8vp_MWBzcPo,29895
52
+ npcpy-1.2.30.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
53
+ npcpy-1.2.30.dist-info/top_level.txt,sha256=g1pbSvrOOncB74Bg5-J0Olg4V0A5VzDw-Xz5YObq8BU,6
54
+ npcpy-1.2.30.dist-info/RECORD,,
File without changes