npcpy 1.2.34__py3-none-any.whl → 1.2.36__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
@@ -20,7 +20,8 @@ from sqlalchemy import create_engine, text
20
20
  import npcpy as npy
21
21
  from npcpy.llm_funcs import DEFAULT_ACTION_SPACE
22
22
  from npcpy.tools import auto_tools
23
-
23
+ import math
24
+ import random
24
25
  from npcpy.npc_sysenv import (
25
26
  ensure_dirs_exist,
26
27
  init_db_tables,
@@ -259,16 +260,18 @@ class Jinx:
259
260
  self._load_from_data(jinx_data)
260
261
  else:
261
262
  raise ValueError("Either jinx_data or jinx_path must be provided")
262
-
263
+
264
+ # Keep a copy for macro expansion, but retain the executable steps by default
263
265
  self._raw_steps = list(self.steps)
264
- self.steps = []
265
-
266
+ self.steps = list(self._raw_steps)
266
267
  def _load_from_file(self, path):
267
268
  jinx_data = load_yaml_file(path)
268
269
  if not jinx_data:
269
270
  raise ValueError(f"Failed to load jinx from {path}")
271
+ self._source_path = path
270
272
  self._load_from_data(jinx_data)
271
273
 
274
+
272
275
  def _load_from_data(self, jinx_data):
273
276
  if not jinx_data or not isinstance(jinx_data, dict):
274
277
  raise ValueError("Invalid jinx data provided")
@@ -281,6 +284,7 @@ class Jinx:
281
284
  self.description = jinx_data.get("description", "")
282
285
  self.npc = jinx_data.get("npc")
283
286
  self.steps = jinx_data.get("steps", [])
287
+ self._source_path = jinx_data.get("_source_path", None)
284
288
 
285
289
  def render_first_pass(
286
290
  self,
@@ -450,6 +454,10 @@ class Jinx:
450
454
  "__builtins__": __builtins__,
451
455
  "npc": active_npc,
452
456
  "context": context,
457
+ "math": math,
458
+ "random": random,
459
+ "datetime": datetime,
460
+ "Image": Image,
453
461
  "pd": pd,
454
462
  "plt": plt,
455
463
  "sys": sys,
@@ -611,6 +619,35 @@ def load_jinxs_from_directory(directory):
611
619
 
612
620
  return jinxs
613
621
 
622
+ def jinx_to_tool_def(jinx_obj: 'Jinx') -> Dict[str, Any]:
623
+ """Convert a Jinx instance into an MCP/LLM-compatible tool schema definition."""
624
+ properties: Dict[str, Any] = {}
625
+ required: List[str] = []
626
+ for inp in jinx_obj.inputs:
627
+ if isinstance(inp, str):
628
+ properties[inp] = {"type": "string"}
629
+ required.append(inp)
630
+ elif isinstance(inp, dict):
631
+ name = list(inp.keys())[0]
632
+ properties[name] = {"type": "string", "default": inp.get(name, "")}
633
+ required.append(name)
634
+ return {
635
+ "type": "function",
636
+ "function": {
637
+ "name": jinx_obj.jinx_name,
638
+ "description": jinx_obj.description or f"Jinx: {jinx_obj.jinx_name}",
639
+ "parameters": {
640
+ "type": "object",
641
+ "properties": properties,
642
+ "required": required
643
+ }
644
+ }
645
+ }
646
+
647
+ def build_jinx_tool_catalog(jinxs: Dict[str, 'Jinx']) -> Dict[str, Dict[str, Any]]:
648
+ """Helper to build a name->tool_def catalog from a dict of Jinx objects."""
649
+ return {name: jinx_to_tool_def(jinx_obj) for name, jinx_obj in jinxs.items()}
650
+
614
651
  def get_npc_action_space(npc=None, team=None):
615
652
  """Get action space for NPC including memory CRUD and core capabilities"""
616
653
  actions = DEFAULT_ACTION_SPACE.copy()
@@ -618,8 +655,9 @@ def get_npc_action_space(npc=None, team=None):
618
655
  if npc:
619
656
  core_tools = [
620
657
  npc.think_step_by_step,
621
- npc.write_code
622
658
  ]
659
+ if hasattr(npc, "write_code"):
660
+ core_tools.append(npc.write_code)
623
661
 
624
662
  if npc.command_history:
625
663
  core_tools.extend([
@@ -842,6 +880,8 @@ class NPC:
842
880
  self.tools_schema = []
843
881
  self.plain_system_message = plain_system_message
844
882
  self.use_global_jinxs = use_global_jinxs
883
+ self.jinx_tool_catalog: Dict[str, Dict[str, Any]] = {}
884
+ self.mcp_servers = []
845
885
 
846
886
  self.memory_length = 20
847
887
  self.memory_strategy = 'recent'
@@ -877,20 +917,20 @@ class NPC:
877
917
  # If jinxs are explicitly provided *to the NPC* during its standalone creation, load them.
878
918
  # This is for NPCs created *outside* a team context initially.
879
919
  if jinxs and jinxs != "*":
880
- for jinx_item in jinxs:
881
- if isinstance(jinx_item, Jinx):
882
- self.jinxs_dict[jinx_item.jinx_name] = jinx_item
883
- elif isinstance(jinx_item, dict):
884
- jinx_obj = Jinx(jinx_data=jinx_item)
885
- self.jinxs_dict[jinx_obj.jinx_name] = jinx_obj
886
- elif isinstance(jinx_item, str):
887
- # Try to load from NPC's own directory first
888
- jinx_path = find_file_path(jinx_item, [self.npc_jinxs_directory], suffix=".jinx")
889
- if jinx_path:
890
- jinx_obj = Jinx(jinx_path=jinx_path)
891
- self.jinxs_dict[jinx_obj.jinx_name] = jinx_obj
892
- else:
893
- print(f"Warning: Jinx '{jinx_item}' not found for NPC '{self.name}' during initial load.")
920
+ for jinx_item in jinxs:
921
+ if isinstance(jinx_item, Jinx):
922
+ self.jinxs_dict[jinx_item.jinx_name] = jinx_item
923
+ elif isinstance(jinx_item, dict):
924
+ jinx_obj = Jinx(jinx_data=jinx_item)
925
+ self.jinxs_dict[jinx_obj.jinx_name] = jinx_obj
926
+ elif isinstance(jinx_item, str):
927
+ # Try to load from NPC's own directory first
928
+ jinx_path = find_file_path(jinx_item, [self.npc_jinxs_directory], suffix=".jinx")
929
+ if jinx_path:
930
+ jinx_obj = Jinx(jinx_path=jinx_path)
931
+ self.jinxs_dict[jinx_obj.jinx_name] = jinx_obj
932
+ else:
933
+ print(f"Warning: Jinx '{jinx_item}' not found for NPC '{self.name}' during initial load.")
894
934
 
895
935
  self.shared_context = {
896
936
  "dataframes": {},
@@ -968,7 +1008,8 @@ class NPC:
968
1008
  except Exception as e:
969
1009
  print(f"Error performing first-pass rendering for NPC Jinx '{raw_npc_jinx.jinx_name}': {e}")
970
1010
 
971
- print(f"NPC {self.name} loaded {len(self.jinxs_dict)} jinxs.")
1011
+ self.jinx_tool_catalog = build_jinx_tool_catalog(self.jinxs_dict)
1012
+ print(f"NPC {self.name} loaded {len(self.jinxs_dict)} jinxs and built catalog with {len(self.jinx_tool_catalog)} tools.")
972
1013
 
973
1014
  def _load_npc_kg(self):
974
1015
  """Load knowledge graph data for this NPC from database"""
@@ -1995,6 +2036,7 @@ class Team:
1995
2036
  self.sub_teams: Dict[str, 'Team'] = {}
1996
2037
  self.jinxs_dict: Dict[str, 'Jinx'] = {} # This will store first-pass rendered Jinx objects
1997
2038
  self._raw_jinxs_list: List['Jinx'] = [] # Temporary storage for raw Team-level Jinx objects
2039
+ self.jinx_tool_catalog: Dict[str, Dict[str, Any]] = {} # Jinx-derived tool defs ready for MCP/LLM
1998
2040
 
1999
2041
  self.jinja_env_for_first_pass = Environment(undefined=SilentUndefined) # Env for macro expansion
2000
2042
 
@@ -2050,6 +2092,8 @@ class Team:
2050
2092
 
2051
2093
  # Perform first-pass rendering for team-level jinxs
2052
2094
  self._perform_first_pass_jinx_rendering()
2095
+ self.jinx_tool_catalog = build_jinx_tool_catalog(self.jinxs_dict)
2096
+ print(f"[TEAM] Built Jinx tool catalog with {len(self.jinx_tool_catalog)} entries for team {self.name}")
2053
2097
 
2054
2098
  # Now, initialize jinxs for all NPCs, as team-level jinxs are ready
2055
2099
  for npc_obj in self.npcs.values():
@@ -2227,8 +2271,6 @@ class Team:
2227
2271
  self.jinxs_dict[raw_jinx.jinx_name] = raw_jinx # Store the first-pass rendered Jinx
2228
2272
  except Exception as e:
2229
2273
  print(f"Error performing first-pass rendering for Jinx '{raw_jinx.jinx_name}': {e}")
2230
-
2231
- self._raw_jinxs_list = [] # Clear temporary storage
2232
2274
 
2233
2275
 
2234
2276
  def update_context(self, messages: list):