npcpy 1.2.37__py3-none-any.whl → 1.3.1__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/npc_compiler.py CHANGED
@@ -384,7 +384,6 @@ def write_yaml_file(file_path, data):
384
384
  print(f"Error writing YAML file {file_path}: {e}")
385
385
  return False
386
386
 
387
-
388
387
  class Jinx:
389
388
  '''
390
389
  Jinx represents a workflow template with Jinja-rendered steps.
@@ -394,7 +393,8 @@ class Jinx:
394
393
  - inputs: list of input parameters
395
394
  - description: what the jinx does
396
395
  - npc: optional NPC to execute with
397
- - steps: list of step definitions with code
396
+ - steps: list of step definitions with code. This section can now be a Jinja template itself.
397
+ - file_context: optional list of file patterns to include as context
398
398
 
399
399
  Execution:
400
400
  - Renders Jinja templates in step code with input values
@@ -409,9 +409,13 @@ class Jinx:
409
409
  else:
410
410
  raise ValueError("Either jinx_data or jinx_path must be provided")
411
411
 
412
- # Keep a copy for macro expansion, but retain the executable steps by default
412
+ # _raw_steps will now hold the original, potentially templated, steps definition
413
413
  self._raw_steps = list(self.steps)
414
- self.steps = list(self._raw_steps)
414
+ self.steps = [] # Will be populated after first-pass rendering
415
+ self.parsed_files = {}
416
+ if self.file_context:
417
+ self.parsed_files = self._parse_file_patterns(self.file_context)
418
+
415
419
  def _load_from_file(self, path):
416
420
  jinx_data = load_yaml_file(path)
417
421
  if not jinx_data:
@@ -431,7 +435,8 @@ class Jinx:
431
435
  self.inputs = jinx_data.get("inputs", [])
432
436
  self.description = jinx_data.get("description", "")
433
437
  self.npc = jinx_data.get("npc")
434
- self.steps = jinx_data.get("steps", [])
438
+ self.steps = jinx_data.get("steps", []) # This can now be a Jinja templated list
439
+ self.file_context = jinx_data.get("file_context", [])
435
440
  self._source_path = jinx_data.get("_source_path", None)
436
441
 
437
442
  def to_tool_def(self) -> Dict[str, Any]:
@@ -469,21 +474,52 @@ class Jinx:
469
474
  ):
470
475
  """
471
476
  Performs the first-pass Jinja rendering on the Jinx's raw steps.
472
- This expands nested Jinx calls (e.g., {{ sh(...) }} or
473
- engine: jinx_name) but preserves runtime variables
474
- (e.g., {{ command_var }}).
475
-
476
- Args:
477
- jinja_env_for_macros: The Jinja Environment configured with
478
- Jinx callables in its globals.
479
- all_jinx_callables: A dictionary of Jinx names to their
480
- callable functions (from create_jinx_callable).
477
+ This expands Jinja control flow (for, if) to generate step structures,
478
+ then expands nested Jinx calls (e.g., {{ sh(...) }} or engine: jinx_name)
479
+ and inline macros.
481
480
  """
482
- rendered_steps_output = []
481
+ # 1. Join the list of raw steps (which are individual YAML lines) into a single string.
482
+ # This single string is the complete Jinja template for the 'steps' section.
483
+ raw_steps_template_string = "\n".join(self._raw_steps)
484
+
485
+ # 2. Render this single string as a Jinja template.
486
+ # Jinja will now process the {% for %} and {% if %} directives,
487
+ # dynamically generating the YAML structure.
488
+ try:
489
+ steps_template = jinja_env_for_macros.from_string(raw_steps_template_string)
490
+ # Pass globals (like num_tasks, include_greeting from Jinx inputs)
491
+ # to the Jinja rendering context for structural templating.
492
+ rendered_steps_yaml_string = steps_template.render(**jinja_env_for_macros.globals)
493
+ except Exception as e:
494
+ # In a real Jinx, this would go to a proper logger.
495
+ # For this context, we handle the error gracefully.
496
+ # self._log_debug(f"Warning: Error during first-pass templating of Jinx '{self.jinx_name}' steps YAML: {e}")
497
+ self.steps = list(self._raw_steps) # Fallback to original raw steps
498
+ return
483
499
 
484
- for raw_step in self._raw_steps:
500
+ # 3. Parse the rendered YAML string back into a list of step dictionaries.
501
+ # This step will now correctly interpret the YAML structure generated by Jinja.
502
+ try:
503
+ structurally_expanded_steps = yaml.safe_load(rendered_steps_yaml_string)
504
+ if not isinstance(structurally_expanded_steps, list):
505
+ # Handle cases where the rendered YAML might be empty or not a list
506
+ if structurally_expanded_steps is None:
507
+ structurally_expanded_steps = []
508
+ else:
509
+ raise ValueError(f"Rendered steps YAML did not result in a list: {type(structurally_expanded_steps)}")
510
+ self.steps = structurally_expanded_steps
511
+ except Exception as e:
512
+ # self._log_debug(f"Warning: Error re-parsing structurally expanded steps YAML for Jinx '{self.jinx_name}': {e}")
513
+ self.steps = list(self._raw_steps) # Fallback
514
+ return
515
+
516
+ # 4. Now, iterate through these `structurally_expanded_steps` to expand
517
+ # declarative Jinx calls (engine: jinx_name) and inline macros.
518
+ # This is the second phase of the first-pass rendering.
519
+ final_rendered_steps = []
520
+ for raw_step in structurally_expanded_steps:
485
521
  if not isinstance(raw_step, dict):
486
- rendered_steps_output.append(raw_step)
522
+ final_rendered_steps.append(raw_step)
487
523
  continue
488
524
 
489
525
  engine_name = raw_step.get('engine')
@@ -502,51 +538,58 @@ class Jinx:
502
538
  expanded_steps = yaml.safe_load(expanded_yaml_string)
503
539
 
504
540
  if isinstance(expanded_steps, list):
505
- rendered_steps_output.extend(expanded_steps)
506
-
541
+ final_rendered_steps.extend(expanded_steps)
507
542
  elif expanded_steps is not None:
508
- rendered_steps_output.append(expanded_steps)
509
-
543
+ final_rendered_steps.append(expanded_steps)
510
544
  except Exception as e:
511
- print(
512
- f"Warning: Error expanding Jinx '{engine_name}' "
513
- f"within Jinx '{self.jinx_name}' "
514
- f"(declarative): {e}"
515
- )
516
- rendered_steps_output.append(raw_step)
517
- # Skip rendering for python/bash engine steps - preserve runtime variables
545
+ # self._log_debug(
546
+ # f"Warning: Error expanding Jinx '{engine_name}' "
547
+ # f"within Jinx '{self.jinx_name}' "
548
+ # f"(declarative): {e}"
549
+ # )
550
+ final_rendered_steps.append(raw_step)
551
+ # For python/bash engine steps, only inline macro expansion happens in the next block.
552
+ # The code content itself is preserved for runtime Jinja rendering.
518
553
  elif raw_step.get('engine') in ['python', 'bash']:
519
- rendered_steps_output.append(raw_step)
554
+ processed_step = {}
555
+ for key, value in raw_step.items():
556
+ if isinstance(value, str):
557
+ try:
558
+ template = jinja_env_for_macros.from_string(value)
559
+ # Render with empty context for inline macros/static values
560
+ rendered_value = template.render({})
561
+ try:
562
+ loaded_value = yaml.safe_load(rendered_value)
563
+ processed_step[key] = loaded_value
564
+ except yaml.YAMLError:
565
+ processed_step[key] = rendered_value
566
+ except Exception as e:
567
+ # self._log_debug(f"Warning: Error during first-pass rendering of Jinx '{self.jinx_name}' step field '{key}' (inline macro): {e}")
568
+ processed_step[key] = value
569
+ else:
570
+ processed_step[key] = value
571
+ final_rendered_steps.append(processed_step)
520
572
  else:
521
- # For other steps, do first-pass rendering (inline macro expansion)
573
+ # For other steps (e.g., custom engines, or just data), perform inline macro expansion
522
574
  processed_step = {}
523
575
  for key, value in raw_step.items():
524
576
  if isinstance(value, str):
525
577
  try:
526
- template = jinja_env_for_macros.from_string(
527
- value
528
- )
578
+ template = jinja_env_for_macros.from_string(value)
529
579
  rendered_value = template.render({})
530
-
531
580
  try:
532
- loaded_value = yaml.safe_load(
533
- rendered_value
534
- )
581
+ loaded_value = yaml.safe_load(rendered_value)
535
582
  processed_step[key] = loaded_value
536
583
  except yaml.YAMLError:
537
584
  processed_step[key] = rendered_value
538
585
  except Exception as e:
539
- print(
540
- f"Warning: Error during first-pass "
541
- f"rendering of Jinx '{self.jinx_name}' "
542
- f"step field '{key}' (inline macro): {e}"
543
- )
586
+ # self._log_debug(f"Warning: Error during first-pass rendering of Jinx '{self.jinx_name}' step field '{key}' (inline macro): {e}")
544
587
  processed_step[key] = value
545
588
  else:
546
589
  processed_step[key] = value
547
- rendered_steps_output.append(processed_step)
590
+ final_rendered_steps.append(processed_step)
548
591
 
549
- self.steps = rendered_steps_output
592
+ self.steps = final_rendered_steps
550
593
 
551
594
  def execute(self,
552
595
  input_values: Dict[str, Any],
@@ -575,6 +618,11 @@ class Jinx:
575
618
  "messages": messages,
576
619
  "npc": active_npc
577
620
  })
621
+
622
+ # Add parsed file content to the context
623
+ if self.parsed_files:
624
+ context['file_context'] = self._format_parsed_files_context(self.parsed_files)
625
+ context['files'] = self.parsed_files # Also make raw dict available
578
626
 
579
627
  for i, step in enumerate(self.steps):
580
628
  context = self._execute_step(
@@ -585,9 +633,19 @@ class Jinx:
585
633
  messages=messages,
586
634
  extra_globals=extra_globals
587
635
  )
636
+ # If an error occurred in a step, propagate it and stop execution
637
+ if "error" in context.get("output", ""):
638
+ self._log_debug(f"DEBUG: Jinx '{self.jinx_name}' execution stopped due to error in step '{step.get('name', 'unnamed_step')}': {context['output']}")
639
+ break
588
640
 
589
641
  return context
590
642
 
643
+ def _log_debug(self, msg: str):
644
+ """Helper for logging debug messages to a file."""
645
+ log_file_path = os.path.expanduser("~/jinx_debug_log.txt")
646
+ with open(log_file_path, "a") as f:
647
+ f.write(f"[{datetime.now().isoformat()}] {msg}\n")
648
+
591
649
  def _execute_step(self,
592
650
  step: Dict[str, Any],
593
651
  context: Dict[str, Any],
@@ -596,40 +654,31 @@ class Jinx:
596
654
  messages: Optional[List[Dict[str, str]]] = None,
597
655
  extra_globals: Optional[Dict[str, Any]] = None):
598
656
 
599
- def _log_debug(msg):
600
- log_file_path = os.path.expanduser("~/jinx_debug_log.txt")
601
- with open(log_file_path, "a") as f:
602
- f.write(f"[{datetime.now().isoformat()}] {msg}\n")
603
-
604
657
  code_content = step.get("code", "")
605
658
  step_name = step.get("name", "unnamed_step")
606
659
  step_npc = step.get("npc")
607
660
 
608
661
  active_npc = step_npc if step_npc else npc
609
662
 
663
+ # Second pass Jinja rendering: render the step's code with the current runtime context
610
664
  try:
611
665
  template = jinja_env.from_string(code_content)
612
666
  rendered_code = template.render(**context)
613
667
  except Exception as e:
614
- _log_debug(
615
- f"Error rendering template for step {step_name} "
616
- f"(second pass): {e}"
668
+ error_msg = (
669
+ f"Error rendering template for step '{step_name}' "
670
+ f"(second pass): {type(e).__name__}: {e}"
617
671
  )
618
- rendered_code = code_content
672
+ context['output'] = error_msg
673
+ self._log_debug(error_msg)
674
+ return context
619
675
 
620
- _log_debug(f"rendered jinx code: {rendered_code}")
621
- _log_debug(
622
- f"DEBUG: Before exec - rendered_code: {rendered_code}"
623
- )
624
- _log_debug(
625
- f"DEBUG: Before exec - context['output'] before step: "
626
- f"{context.get('output')}"
627
- )
676
+ self._log_debug(f"DEBUG: Executing step '{step_name}' with rendered code: {rendered_code}")
628
677
 
629
678
  exec_globals = {
630
679
  "__builtins__": __builtins__,
631
680
  "npc": active_npc,
632
- "context": context,
681
+ "context": context, # Pass context by reference
633
682
  "math": math,
634
683
  "random": random,
635
684
  "datetime": datetime,
@@ -653,43 +702,27 @@ class Jinx:
653
702
  if extra_globals:
654
703
  exec_globals.update(extra_globals)
655
704
 
656
- exec_locals = {}
657
-
705
+ exec_locals = {} # Locals for this specific exec call
706
+
658
707
  try:
659
708
  exec(rendered_code, exec_globals, exec_locals)
660
709
  except Exception as e:
661
710
  error_msg = (
662
- f"Error executing step {step_name}: "
711
+ f"Error executing step '{step_name}': "
663
712
  f"{type(e).__name__}: {e}"
664
713
  )
665
714
  context['output'] = error_msg
666
- _log_debug(error_msg)
715
+ self._log_debug(error_msg)
667
716
  return context
668
717
 
669
- _log_debug(f"DEBUG: After exec - exec_locals: {exec_locals}")
670
- _log_debug(
671
- f"DEBUG: After exec - 'output' in exec_locals: "
672
- f"{'output' in exec_locals}"
673
- )
674
-
718
+ # Update the main context with any variables set in exec_locals
675
719
  context.update(exec_locals)
676
720
 
677
- _log_debug(
678
- f"DEBUG: After context.update(exec_locals) - "
679
- f"context['output']: {context.get('output')}"
680
- )
681
- _log_debug(f"context after jinx ex: {context}")
682
-
683
721
  if "output" in exec_locals:
684
722
  outp = exec_locals["output"]
685
723
  context["output"] = outp
686
724
  context[step_name] = outp
687
725
 
688
- _log_debug(
689
- f"DEBUG: Inside 'output' in exec_locals block - "
690
- f"context['output']: {context.get('output')}"
691
- )
692
-
693
726
  if messages is not None:
694
727
  messages.append({
695
728
  'role':'assistant',
@@ -702,13 +735,95 @@ class Jinx:
702
735
 
703
736
  return context
704
737
 
738
+ def _parse_file_patterns(self, patterns_config):
739
+ """Parse file patterns configuration and load matching files into KV cache"""
740
+ if not patterns_config:
741
+ return {}
742
+
743
+ file_cache = {}
744
+
745
+ for pattern_entry in patterns_config:
746
+ if isinstance(pattern_entry, str):
747
+ pattern_entry = {"pattern": pattern_entry}
748
+
749
+ pattern = pattern_entry.get("pattern", "")
750
+ recursive = pattern_entry.get("recursive", False)
751
+ base_path = pattern_entry.get("base_path", ".")
752
+
753
+ if not pattern:
754
+ continue
755
+
756
+ # Resolve base_path relative to jinx's source path or current working directory
757
+ if self._source_path:
758
+ base_path = os.path.join(os.path.dirname(self._source_path), base_path)
759
+ base_path = os.path.expanduser(base_path)
760
+
761
+ if not os.path.isabs(base_path):
762
+ base_path = os.path.join(os.getcwd(), base_path)
763
+
764
+ matching_files = self._find_matching_files(pattern, base_path, recursive)
765
+
766
+ for file_path in matching_files:
767
+ file_content = self._load_file_content(file_path)
768
+ if file_content:
769
+ relative_path = os.path.relpath(file_path, base_path)
770
+ file_cache[relative_path] = file_content
771
+
772
+ return file_cache
773
+
774
+ def _find_matching_files(self, pattern, base_path, recursive=False):
775
+ """Find files matching the given pattern"""
776
+ matching_files = []
777
+
778
+ if not os.path.exists(base_path):
779
+ return matching_files
780
+
781
+ if recursive:
782
+ for root, dirs, files in os.walk(base_path):
783
+ for filename in files:
784
+ if fnmatch.fnmatch(filename, pattern):
785
+ matching_files.append(os.path.join(root, filename))
786
+ else:
787
+ try:
788
+ for item in os.listdir(base_path):
789
+ item_path = os.path.join(base_path, item)
790
+ if os.path.isfile(item_path) and fnmatch.fnmatch(item, pattern):
791
+ matching_files.append(item_path)
792
+ except PermissionError:
793
+ print(f"Permission denied accessing {base_path}")
794
+
795
+ return matching_files
796
+
797
+ def _load_file_content(self, file_path):
798
+ """Load content from a file with error handling"""
799
+ try:
800
+ with open(file_path, 'r', encoding='utf-8') as f:
801
+ return f.read()
802
+ except Exception as e:
803
+ print(f"Error reading {file_path}: {e}")
804
+ return None
805
+
806
+ def _format_parsed_files_context(self, parsed_files):
807
+ """Format parsed files into context string"""
808
+ if not parsed_files:
809
+ return ""
810
+
811
+ context_parts = ["Additional context from files:"]
812
+
813
+ for file_path, content in parsed_files.items():
814
+ context_parts.append(f"\n--- {file_path} ---")
815
+ context_parts.append(content)
816
+ context_parts.append("")
817
+
818
+ return "\n".join(context_parts)
705
819
 
706
820
  def to_dict(self):
707
821
  result = {
708
822
  "jinx_name": self.jinx_name,
709
823
  "description": self.description,
710
824
  "inputs": self.inputs,
711
- "steps": self._raw_steps
825
+ "steps": self._raw_steps, # Save the original raw steps, which might be templated
826
+ "file_context": self.file_context
712
827
  }
713
828
 
714
829
  if self.npc:
@@ -754,6 +869,7 @@ class Jinx:
754
869
  "jinx_name": name,
755
870
  "description": doc.strip(),
756
871
  "inputs": inputs,
872
+ "file_context": [],
757
873
  "steps": [
758
874
  {
759
875
  "name": "mcp_function_call",
@@ -775,6 +891,8 @@ output = {mcp_tool.__module__}.{name}(
775
891
  except:
776
892
  pass
777
893
 
894
+
895
+
778
896
  def load_jinxs_from_directory(directory):
779
897
  """Load all jinxs from a directory recursively"""
780
898
  jinxs = []
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: npcpy
3
- Version: 1.2.37
3
+ Version: 1.3.1
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
@@ -275,9 +275,8 @@ output = context['research_summary']
275
275
  "code": '''
276
276
  # Access outputs from previous steps.
277
277
  research_summary = context['initial_llm_research']
278
- # The output of a declarative jinx call (like 'file_reader') is stored under its step name.
279
- # The actual content we want is the 'output' of the *last step* within that sub-jinx.
280
- file_summary = context['read_and_process_source_file'].get('output', 'No file summary available.')
278
+ # The file_reader jinx returns its output directly; also keep a fallback of file_raw_content.
279
+ file_summary = context.get('read_and_process_source_file', '') or context.get('file_raw_content', 'No file summary available.')
281
280
 
282
281
  prompt = f"""Based on the following information:
283
282
  1. Comprehensive Research Summary:
@@ -3,7 +3,7 @@ npcpy/llm_funcs.py,sha256=KJpjN6q5iW_qdUfgt4tzYENCAu86376io8eFZ7wp76Y,78081
3
3
  npcpy/main.py,sha256=RWoRIj6VQLxKdOKvdVyaq2kwG35oRpeXPvp1CAAoG-w,81
4
4
  npcpy/ml_funcs.py,sha256=UI7k7JR4XOH_VXR-xxLaO4r9Kyx_jBaEnp3TUIY7ZLQ,22657
5
5
  npcpy/npc_array.py,sha256=fVTxcMiXV-lvltmuwaRnTU9D3ikPq3-7k5wzp7MA5OY,40224
6
- npcpy/npc_compiler.py,sha256=oGSn9-X-Miq-K37QfNo8_TFcWbOl8WTNHBPtkf6paws,104619
6
+ npcpy/npc_compiler.py,sha256=956ZMSSrYmVRp52-A4-wasg6wey3QIWHGGirDL-dW8o,111498
7
7
  npcpy/npc_sysenv.py,sha256=rtE3KrXvIuOEpMq1CW5eK5K0o3f6mXagNXCeMnhHob4,36736
8
8
  npcpy/npcs.py,sha256=eExuVsbTfrRobTRRptRpDm46jCLWUgbvy4_U7IUQo-c,744
9
9
  npcpy/serve.py,sha256=wbIXUFlmfKg72ZYoX_cBJ8FVDFabHsGnbMwMIj-412Y,174839
@@ -50,8 +50,8 @@ npcpy/work/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
50
50
  npcpy/work/desktop.py,sha256=F3I8mUtJp6LAkXodsh8hGZIncoads6c_2Utty-0EdDA,2986
51
51
  npcpy/work/plan.py,sha256=QyUwg8vElWiHuoS-xK4jXTxxHvkMD3VkaCEsCmrEPQk,8300
52
52
  npcpy/work/trigger.py,sha256=P1Y8u1wQRsS2WACims_2IdkBEar-iBQix-2TDWoW0OM,9948
53
- npcpy-1.2.37.dist-info/licenses/LICENSE,sha256=j0YPvce7Ng9e32zYOu0EmXjXeJ0Nwawd0RA3uSGGH4E,1070
54
- npcpy-1.2.37.dist-info/METADATA,sha256=xviLiFxgZ1uK876GGrIIm-P3uEJlwBvCtbbgn0vFUP4,37940
55
- npcpy-1.2.37.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
56
- npcpy-1.2.37.dist-info/top_level.txt,sha256=g1pbSvrOOncB74Bg5-J0Olg4V0A5VzDw-Xz5YObq8BU,6
57
- npcpy-1.2.37.dist-info/RECORD,,
53
+ npcpy-1.3.1.dist-info/licenses/LICENSE,sha256=j0YPvce7Ng9e32zYOu0EmXjXeJ0Nwawd0RA3uSGGH4E,1070
54
+ npcpy-1.3.1.dist-info/METADATA,sha256=dbTBVm4ZMDwwnGkOC3Ahwwhf9OpcecBO_Tdww1ToUwE,37884
55
+ npcpy-1.3.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
56
+ npcpy-1.3.1.dist-info/top_level.txt,sha256=g1pbSvrOOncB74Bg5-J0Olg4V0A5VzDw-Xz5YObq8BU,6
57
+ npcpy-1.3.1.dist-info/RECORD,,
File without changes