npcpy 1.2.32__py3-none-any.whl → 1.2.33__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
@@ -13,8 +13,8 @@ import hashlib
13
13
  import pathlib
14
14
  import fnmatch
15
15
  import subprocess
16
- from typing import Any, Dict, List, Optional, Union
17
- from jinja2 import Environment, FileSystemLoader, Template, Undefined
16
+ from typing import Any, Dict, List, Optional, Union, Callable, Tuple
17
+ from jinja2 import Environment, FileSystemLoader, Template, Undefined, DictLoader
18
18
  from sqlalchemy import create_engine, text
19
19
  import npcpy as npy
20
20
  from npcpy.llm_funcs import DEFAULT_ACTION_SPACE
@@ -228,16 +228,23 @@ def write_yaml_file(file_path, data):
228
228
  print(f"Error writing YAML file {file_path}: {e}")
229
229
  return False
230
230
 
231
-
232
231
  class Jinx:
233
232
  '''
233
+ Jinx represents a workflow template with Jinja-rendered steps.
234
+
235
+ Loads YAML definition containing:
236
+ - jinx_name: identifier
237
+ - inputs: list of input parameters
238
+ - description: what the jinx does
239
+ - npc: optional NPC to execute with
240
+ - steps: list of step definitions with code
234
241
 
235
- Jinx is a class that provides methods for rendering jinja templates to execute
236
- natural language commands within the NPC ecosystem, python, and eventually
237
- other code languages.
242
+ Execution:
243
+ - Renders Jinja templates in step code with input values
244
+ - Executes resulting Python code
245
+ - Returns context with outputs
238
246
  '''
239
247
  def __init__(self, jinx_data=None, jinx_path=None):
240
- """Initialize a jinx from data or file path"""
241
248
  if jinx_path:
242
249
  self._load_from_file(jinx_path)
243
250
  elif jinx_data:
@@ -245,15 +252,17 @@ class Jinx:
245
252
  else:
246
253
  raise ValueError("Either jinx_data or jinx_path must be provided")
247
254
 
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
258
+
248
259
  def _load_from_file(self, path):
249
- """Load jinx from file"""
250
260
  jinx_data = load_yaml_file(path)
251
261
  if not jinx_data:
252
262
  raise ValueError(f"Failed to load jinx from {path}")
253
263
  self._load_from_data(jinx_data)
254
264
 
255
265
  def _load_from_data(self, jinx_data):
256
- """Load jinx from data dictionary"""
257
266
  if not jinx_data or not isinstance(jinx_data, dict):
258
267
  raise ValueError("Invalid jinx data provided")
259
268
 
@@ -263,62 +272,147 @@ class Jinx:
263
272
  self.jinx_name = jinx_data.get("jinx_name")
264
273
  self.inputs = jinx_data.get("inputs", [])
265
274
  self.description = jinx_data.get("description", "")
266
- self.steps = self._parse_steps(jinx_data.get("steps", []))
267
- def _parse_steps(self, steps):
268
- """Parse steps from jinx definition"""
269
- parsed_steps = []
270
- for i, step in enumerate(steps):
271
- if isinstance(step, dict):
272
- parsed_step = {
273
- "name": step.get("name", f"step_{i}"),
274
- "engine": step.get("engine", "natural"),
275
- "code": step.get("code", "")
275
+ self.npc = jinx_data.get("npc")
276
+ self.steps = jinx_data.get("steps", []) # These are the raw steps initially
277
+
278
+ def render_first_pass(
279
+ self,
280
+ jinja_env_for_macros: Environment,
281
+ all_jinx_callables: Dict[str, Callable]
282
+ ):
283
+ """
284
+ Performs the first-pass Jinja rendering on the Jinx's raw steps.
285
+ This expands nested Jinx calls (e.g., {{ sh(...) }} or
286
+ engine: jinx_name) but preserves runtime variables
287
+ (e.g., {{ command_var }}).
288
+
289
+ Args:
290
+ jinja_env_for_macros: The Jinja Environment configured with
291
+ Jinx callables in its globals.
292
+ all_jinx_callables: A dictionary of Jinx names to their
293
+ callable functions (from create_jinx_callable).
294
+ """
295
+ rendered_steps_output = []
296
+
297
+ for raw_step in self._raw_steps:
298
+ if not isinstance(raw_step, dict):
299
+ rendered_steps_output.append(raw_step)
300
+ continue
301
+
302
+ engine_name = raw_step.get('engine')
303
+
304
+ if engine_name and engine_name in all_jinx_callables:
305
+ step_name = raw_step.get('name', f'call_{engine_name}')
306
+ jinx_args = {
307
+ k: v for k, v in raw_step.items()
308
+ if k not in ['engine', 'name']
276
309
  }
277
- if "mode" in step:
278
- parsed_step["mode"] = step["mode"]
279
- parsed_steps.append(parsed_step)
310
+
311
+ jinx_callable = all_jinx_callables[engine_name]
312
+ try:
313
+ expanded_yaml_string = jinx_callable(**jinx_args)
314
+ expanded_steps = yaml.safe_load(expanded_yaml_string)
315
+
316
+ 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
+ rendered_steps_output.extend(expanded_steps)
330
+
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
+ elif expanded_steps is not None:
343
+ rendered_steps_output.append(expanded_steps)
344
+
345
+ except Exception as e:
346
+ print(
347
+ f"Warning: Error expanding Jinx '{engine_name}' "
348
+ f"within Jinx '{self.jinx_name}' "
349
+ f"(declarative): {e}"
350
+ )
351
+ rendered_steps_output.append(raw_step)
280
352
  else:
281
- raise ValueError(f"Invalid step format: {step}")
282
- return parsed_steps
353
+ processed_step = {}
354
+ for key, value in raw_step.items():
355
+ if isinstance(value, str):
356
+ try:
357
+ template = jinja_env_for_macros.from_string(
358
+ value
359
+ )
360
+ rendered_value = template.render({})
361
+
362
+ try:
363
+ loaded_value = yaml.safe_load(
364
+ rendered_value
365
+ )
366
+ processed_step[key] = loaded_value
367
+ except yaml.YAMLError:
368
+ processed_step[key] = rendered_value
369
+ except Exception as e:
370
+ print(
371
+ f"Warning: Error during first-pass "
372
+ f"rendering of Jinx '{self.jinx_name}' "
373
+ f"step field '{key}' (inline macro): {e}"
374
+ )
375
+ processed_step[key] = value
376
+ else:
377
+ processed_step[key] = value
378
+ rendered_steps_output.append(processed_step)
379
+
380
+ self.steps = rendered_steps_output
283
381
 
284
382
  def execute(self,
285
383
  input_values: Dict[str, Any],
286
- jinxs_dict: Dict[str, 'Jinx'],
287
- jinja_env: Optional[Environment] = None,
288
384
  npc: Optional[Any] = None,
289
385
  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
- """
386
+ extra_globals: Optional[Dict[str, Any]] = None,
387
+ jinja_env: Optional[Environment] = None): # Add jinja_env here for the second pass
388
+
389
+ # If jinja_env is not provided, create a default one for the second pass
295
390
  if jinja_env is None:
296
- from jinja2 import DictLoader
297
391
  jinja_env = Environment(
298
392
  loader=DictLoader({}),
299
393
  undefined=SilentUndefined,
300
394
  )
301
395
 
302
- context = (npc.shared_context.copy() if npc and hasattr(npc, 'shared_context') else {})
396
+ active_npc = self.npc if self.npc else npc
397
+
398
+ context = (active_npc.shared_context.copy() if active_npc and hasattr(active_npc, 'shared_context') else {})
303
399
  context.update(input_values)
304
400
  context.update({
305
- "jinxs": jinxs_dict,
306
401
  "llm_response": None,
307
402
  "output": None,
308
403
  "messages": messages,
404
+ "npc": active_npc
309
405
  })
310
-
311
- # This is the key change: Extract 'extra_globals' from kwargs
312
- extra_globals = kwargs.get('extra_globals')
313
406
 
407
+ # Iterate over self.steps, which are now first-pass rendered
314
408
  for i, step in enumerate(self.steps):
315
409
  context = self._execute_step(
316
410
  step,
317
411
  context,
318
- jinja_env,
319
- npc=npc,
412
+ jinja_env, # This is the second-pass Jinja env
413
+ npc=active_npc,
320
414
  messages=messages,
321
- extra_globals=extra_globals # Pass it down to the step executor
415
+ extra_globals=extra_globals
322
416
  )
323
417
 
324
418
  return context
@@ -326,143 +420,103 @@ class Jinx:
326
420
  def _execute_step(self,
327
421
  step: Dict[str, Any],
328
422
  context: Dict[str, Any],
329
- jinja_env: Environment,
423
+ jinja_env: Environment, # This is for the second pass (runtime vars)
330
424
  npc: Optional[Any] = None,
331
425
  messages: Optional[List[Dict[str, str]]] = None,
332
426
  extra_globals: Optional[Dict[str, Any]] = None):
333
- """
334
- Execute a single step of the jinx.
335
- """
336
- engine = step.get("engine", "natural")
337
- code = step.get("code", "")
427
+
428
+ code_content = step.get("code", "") # Get the code content, which might still have {{ runtime_var }}
338
429
  step_name = step.get("name", "unnamed_step")
339
- mode = step.get("mode", "chat")
340
-
430
+ step_npc = step.get("npc")
431
+
432
+ active_npc = step_npc if step_npc else npc
433
+
434
+ # Second pass rendering: resolve runtime variables in the code content
341
435
  try:
342
- template = jinja_env.from_string(code)
436
+ template = jinja_env.from_string(code_content)
343
437
  rendered_code = template.render(**context)
344
-
345
- engine_template = jinja_env.from_string(engine)
346
- rendered_engine = engine_template.render(**context)
438
+ 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
441
+
442
+ exec_globals = {
443
+ "__builtins__": __builtins__,
444
+ "npc": active_npc,
445
+ "context": context,
446
+ "pd": pd,
447
+ "plt": plt,
448
+ "np": np,
449
+ "os": os,
450
+ 're': re,
451
+ "json": json,
452
+ "Path": pathlib.Path,
453
+ "fnmatch": fnmatch,
454
+ "pathlib": pathlib,
455
+ "subprocess": subprocess,
456
+ "get_llm_response": npy.llm_funcs.get_llm_response,
457
+ "CommandHistory": CommandHistory,
458
+ }
459
+
460
+ if extra_globals:
461
+ exec_globals.update(extra_globals)
347
462
 
463
+ exec_locals = {}
464
+
465
+ try:
466
+ exec(rendered_code, exec_globals, exec_locals)
348
467
  except Exception as e:
349
- print(f"Error rendering templates for step {step_name}: {e}")
350
- rendered_code = code
351
- rendered_engine = engine
352
-
353
- if rendered_engine == "natural":
354
- if rendered_code.strip():
355
- if mode == "agent":
356
- response = npc.get_llm_response(
357
- rendered_code,
358
- context=context,
359
- messages=messages,
360
- auto_process_tool_calls=True,
361
- use_core_tools=True
362
- )
363
- else:
364
- response = npc.get_llm_response(
365
- rendered_code,
366
- context=context,
367
- messages=messages,
368
- )
369
-
370
- response_text = response.get("response", "")
371
- context['output'] = response_text
372
- context["llm_response"] = response_text
373
- context["results"] = response_text
374
- context[step_name] = response_text
375
- context['messages'] = response.get('messages')
468
+ error_msg = f"Error executing step {step_name}: {type(e).__name__}: {e}"
469
+ context['output'] = error_msg
470
+ print(error_msg)
471
+ return context
472
+
473
+ context.update(exec_locals)
474
+
475
+ if "output" in exec_locals:
476
+ outp = exec_locals["output"]
477
+ context["output"] = outp
478
+ context[step_name] = outp
479
+ if messages is not None:
480
+ messages.append({
481
+ 'role':'assistant',
482
+ 'content': f'Jinx {self.jinx_name} step {step_name} executed: {outp}'
483
+ })
484
+ context['messages'] = messages
376
485
 
377
- elif rendered_engine == "python":
378
- # Base globals available to all python jinxes, defined within the library (npcpy)
379
- exec_globals = {
380
- "__builtins__": __builtins__,
381
- "npc": npc,
382
- "context": context,
383
- "pd": pd,
384
- "plt": plt,
385
- "np": np,
386
- "os": os,
387
- 're': re,
388
- "json": json,
389
- "Path": pathlib.Path,
390
- "fnmatch": fnmatch,
391
- "pathlib": pathlib,
392
- "subprocess": subprocess,
393
- "get_llm_response": npy.llm_funcs.get_llm_response,
394
- "CommandHistory": CommandHistory, # This is fine, it's part of npcpy
395
- }
396
-
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)
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
-
410
- context.update(exec_locals)
411
-
412
- if "output" in exec_locals:
413
- outp = exec_locals["output"]
414
- context["output"] = outp
415
- context[step_name] = outp
416
- if messages is not None:
417
- messages.append({'role':'assistant',
418
- 'content': f'Jinx executed with following output: {outp}'})
419
- context['messages'] = messages
420
-
421
- else:
422
- context[step_name] = {"error": f"Unsupported engine: {rendered_engine}"}
423
-
424
486
  return context
487
+
425
488
  def to_dict(self):
426
- """Convert to dictionary representation"""
427
- steps_list = []
428
- for i, step in enumerate(self.steps):
429
- step_dict = {
430
- "name": step.get("name", f"step_{i}"),
431
- "engine": step.get("engine"),
432
- "code": step.get("code")
433
- }
434
- if "mode" in step:
435
- step_dict["mode"] = step["mode"]
436
- steps_list.append(step_dict)
437
-
438
- return {
489
+ result = {
439
490
  "jinx_name": self.jinx_name,
440
491
  "description": self.description,
441
492
  "inputs": self.inputs,
442
- "steps": steps_list
493
+ # When converting to dict, we should save the *raw* steps
494
+ # so that when reloaded, the first pass can be done again.
495
+ "steps": self._raw_steps
443
496
  }
497
+
498
+ if self.npc:
499
+ result["npc"] = self.npc
500
+
501
+ return result
502
+
444
503
  def save(self, directory):
445
- """Save jinx to file"""
446
504
  jinx_path = os.path.join(directory, f"{self.jinx_name}.jinx")
447
505
  ensure_dirs_exist(os.path.dirname(jinx_path))
448
506
  return write_yaml_file(jinx_path, self.to_dict())
449
507
 
450
508
  @classmethod
451
509
  def from_mcp(cls, mcp_tool):
452
- """Convert an MCP tool to NPC jinx format"""
453
-
454
510
  try:
455
511
  import inspect
456
512
 
457
-
458
513
  doc = mcp_tool.__doc__ or ""
459
514
  name = mcp_tool.__name__
460
515
  signature = inspect.signature(mcp_tool)
461
516
 
462
-
463
517
  inputs = []
464
518
  for param_name, param in signature.parameters.items():
465
- if param_name != 'self':
519
+ if param_name != 'self':
466
520
  param_type = param.annotation if param.annotation != inspect.Parameter.empty else None
467
521
  param_default = None if param.default == inspect.Parameter.empty else param.default
468
522
 
@@ -472,7 +526,6 @@ class Jinx:
472
526
  "default": param_default
473
527
  })
474
528
 
475
-
476
529
  jinx_data = {
477
530
  "jinx_name": name,
478
531
  "description": doc.strip(),
@@ -480,9 +533,7 @@ class Jinx:
480
533
  "steps": [
481
534
  {
482
535
  "name": "mcp_function_call",
483
- "engine": "python",
484
536
  "code": f"""
485
-
486
537
  import {mcp_tool.__module__}
487
538
  output = {mcp_tool.__module__}.{name}(
488
539
  {', '.join([f'{inp["name"]}=context.get("{inp["name"]}")' for inp in inputs])}
@@ -495,7 +546,7 @@ output = {mcp_tool.__module__}.{name}(
495
546
  return cls(jinx_data=jinx_data)
496
547
 
497
548
  except:
498
- pass
549
+ pass
499
550
 
500
551
  def load_jinxs_from_directory(directory):
501
552
  """Load all jinxs from a directory recursively"""
@@ -687,8 +738,8 @@ class NPC:
687
738
  name: str = None,
688
739
  primary_directive: str = None,
689
740
  plain_system_message: bool = False,
690
- team = None,
691
- jinxs: list = None,
741
+ team = None, # Can be None initially
742
+ jinxs: list = None, # Explicit jinxs for this NPC
692
743
  tools: list = None,
693
744
  model: str = None,
694
745
  provider: str = None,
@@ -696,7 +747,7 @@ class NPC:
696
747
  api_key: str = None,
697
748
  db_conn=None,
698
749
  use_global_jinxs=False,
699
- memory = False,
750
+ memory = False,
700
751
  **kwargs
701
752
  ):
702
753
  """
@@ -734,7 +785,9 @@ class NPC:
734
785
  self.jinxs_directory = None
735
786
  self.npc_directory = None
736
787
 
737
- self.team = team
788
+ self.team = team # Store the team reference (can be None)
789
+ self.jinxs_spec = jinxs or "*" # Store the jinx specification for later loading
790
+
738
791
  if tools is not None:
739
792
  tools_schema, tool_map = auto_tools(tools)
740
793
  self.tools = tools_schema
@@ -755,6 +808,7 @@ class NPC:
755
808
  if self.jinxs_directory:
756
809
  dirs.append(self.jinxs_directory)
757
810
 
811
+ # This jinja_env is for the *second pass* (runtime variable resolution in Jinx.execute)
758
812
  self.jinja_env = Environment(
759
813
  loader=FileSystemLoader([
760
814
  os.path.expanduser(d) for d in dirs
@@ -764,7 +818,6 @@ class NPC:
764
818
 
765
819
  self.db_conn = db_conn
766
820
 
767
- # these 4 get overwritten if the db conn
768
821
  self.command_history = None
769
822
  self.kg_data = None
770
823
  self.tables = None
@@ -777,9 +830,24 @@ class NPC:
777
830
  self.kg_data = self._load_npc_kg()
778
831
  self.memory = self.get_memory_context()
779
832
 
780
-
781
-
782
- self.jinxs = self._load_npc_jinxs(jinxs or "*")
833
+ self.jinxs_dict = {} # Initialize empty, will be populated by initialize_jinxs
834
+ # If jinxs are explicitly provided *to the NPC* during its standalone creation, load them.
835
+ # This is for NPCs created *outside* a team context initially.
836
+ if jinxs and jinxs != "*":
837
+ for jinx_item in jinxs:
838
+ if isinstance(jinx_item, Jinx):
839
+ self.jinxs_dict[jinx_item.jinx_name] = jinx_item
840
+ elif isinstance(jinx_item, dict):
841
+ jinx_obj = Jinx(jinx_data=jinx_item)
842
+ self.jinxs_dict[jinx_obj.jinx_name] = jinx_obj
843
+ elif isinstance(jinx_item, str):
844
+ # Try to load from NPC's own directory first
845
+ jinx_path = find_file_path(jinx_item, [self.npc_jinxs_directory], suffix=".jinx")
846
+ if jinx_path:
847
+ jinx_obj = Jinx(jinx_path=jinx_path)
848
+ self.jinxs_dict[jinx_obj.jinx_name] = jinx_obj
849
+ else:
850
+ print(f"Warning: Jinx '{jinx_item}' not found for NPC '{self.name}' during initial load.")
783
851
 
784
852
  self.shared_context = {
785
853
  "dataframes": {},
@@ -794,6 +862,71 @@ class NPC:
794
862
  if db_conn is not None:
795
863
  init_db_tables()
796
864
 
865
+ def initialize_jinxs(self, team_raw_jinxs: Optional[List['Jinx']] = None):
866
+ """
867
+ Loads and performs first-pass Jinja rendering for NPC-specific jinxs,
868
+ now that the NPC's team context is fully established.
869
+ """
870
+ npc_jinxs_raw_list = []
871
+
872
+ # If jinxs_spec is "*", inherit all from team
873
+ if self.jinxs_spec == "*":
874
+ if self.team and hasattr(self.team, 'jinxs_dict') and self.team.jinxs_dict:
875
+ self.jinxs_dict.update(self.team.jinxs_dict)
876
+ else: # If specific jinxs are requested, try to get them from team
877
+ for jinx_name in self.jinxs_spec:
878
+ if self.team and jinx_name in self.team.jinxs_dict:
879
+ self.jinxs_dict[jinx_name] = self.team.jinxs_dict[jinx_name]
880
+
881
+ # Load NPC's own jinxs (if not already covered by team or if specific ones are requested)
882
+ if hasattr(self, 'npc_jinxs_directory') and self.npc_jinxs_directory and os.path.exists(self.npc_jinxs_directory):
883
+ for jinx_obj in load_jinxs_from_directory(self.npc_jinxs_directory):
884
+ if jinx_obj.jinx_name not in self.jinxs_dict: # Only add if not already added from team
885
+ npc_jinxs_raw_list.append(jinx_obj)
886
+
887
+ # If there are raw NPC jinxs to render or team_raw_jinxs available
888
+ if npc_jinxs_raw_list or team_raw_jinxs:
889
+ all_available_raw_jinxs = list(team_raw_jinxs or [])
890
+ all_available_raw_jinxs.extend(npc_jinxs_raw_list)
891
+
892
+ combined_raw_jinxs_dict = {j.jinx_name: j for j in all_available_raw_jinxs}
893
+
894
+ npc_first_pass_jinja_env = Environment(undefined=SilentUndefined)
895
+
896
+ jinx_macro_globals = {}
897
+ for raw_jinx in combined_raw_jinxs_dict.values():
898
+ def create_jinx_callable(jinx_obj_in_closure):
899
+ def callable_jinx(**kwargs):
900
+ temp_jinja_env = Environment(undefined=SilentUndefined)
901
+ rendered_target_steps = []
902
+ for target_step in jinx_obj_in_closure._raw_steps:
903
+ temp_rendered_step = {}
904
+ for k, v in target_step.items():
905
+ if isinstance(v, str):
906
+ try:
907
+ temp_rendered_step[k] = temp_jinja_env.from_string(v).render(**kwargs)
908
+ except Exception as e:
909
+ print(f"Warning: Error in Jinx macro '{jinx_obj_in_closure.jinx_name}' rendering step field '{k}' (NPC first pass): {e}")
910
+ temp_rendered_step[k] = v
911
+ else:
912
+ temp_rendered_step[k] = v
913
+ rendered_target_steps.append(temp_rendered_step)
914
+ return yaml.dump(rendered_target_steps, default_flow_style=False)
915
+ return callable_jinx
916
+
917
+ jinx_macro_globals[raw_jinx.jinx_name] = create_jinx_callable(raw_jinx)
918
+
919
+ npc_first_pass_jinja_env.globals.update(jinx_macro_globals)
920
+
921
+ for raw_npc_jinx in npc_jinxs_raw_list:
922
+ try:
923
+ raw_npc_jinx.render_first_pass(npc_first_pass_jinja_env, jinx_macro_globals)
924
+ self.jinxs_dict[raw_npc_jinx.jinx_name] = raw_npc_jinx
925
+ except Exception as e:
926
+ print(f"Error performing first-pass rendering for NPC Jinx '{raw_npc_jinx.jinx_name}': {e}")
927
+
928
+ print(f"NPC {self.name} loaded {len(self.jinxs_dict)} jinxs.")
929
+
797
930
  def _load_npc_kg(self):
798
931
  """Load knowledge graph data for this NPC from database"""
799
932
  if not self.command_history:
@@ -1093,45 +1226,6 @@ class NPC:
1093
1226
  self.tables = None
1094
1227
  self.db_type = None
1095
1228
 
1096
- def _load_npc_jinxs(self, jinxs):
1097
- """Load and process NPC-specific jinxs"""
1098
- npc_jinxs = []
1099
-
1100
- if jinxs == "*":
1101
- if self.team and hasattr(self.team, 'jinxs_dict'):
1102
- for jinx in self.team.jinxs_dict.values():
1103
- npc_jinxs.append(jinx)
1104
- elif self.use_global_jinxs or (hasattr(self, 'jinxs_directory') and self.jinxs_directory):
1105
- jinxs_dir = self.jinxs_directory or os.path.expanduser('~/.npcsh/npc_team/jinxs/')
1106
- if os.path.exists(jinxs_dir):
1107
- npc_jinxs.extend(load_jinxs_from_directory(jinxs_dir))
1108
-
1109
- self.jinxs_dict = {jinx.jinx_name: jinx for jinx in npc_jinxs}
1110
- return npc_jinxs
1111
-
1112
- for jinx in jinxs:
1113
- if isinstance(jinx, Jinx):
1114
- npc_jinxs.append(jinx)
1115
- elif isinstance(jinx, dict):
1116
- npc_jinxs.append(Jinx(jinx_data=jinx))
1117
- elif isinstance(jinx, str):
1118
- jinx_path = None
1119
- jinx_name = jinx
1120
- if not jinx_name.endswith(".jinx"):
1121
- jinx_name += ".jinx"
1122
-
1123
- if hasattr(self, 'jinxs_directory') and self.jinxs_directory and os.path.exists(self.jinxs_directory):
1124
- candidate_path = os.path.join(self.jinxs_directory, jinx_name)
1125
- if os.path.exists(candidate_path):
1126
- jinx_path = candidate_path
1127
-
1128
- if jinx_path:
1129
- jinx_obj = Jinx(jinx_path=jinx_path)
1130
- npc_jinxs.append(jinx_obj)
1131
-
1132
- self.jinxs_dict = {jinx.jinx_name: jinx for jinx in npc_jinxs}
1133
- print(npc_jinxs)
1134
- return npc_jinxs
1135
1229
  def get_llm_response(self,
1136
1230
  request,
1137
1231
  jinxs=None,
@@ -1225,7 +1319,7 @@ class NPC:
1225
1319
  content = result.get('content', '')[:200] + ('...' if len(result.get('content', '')) > 200 else '')
1226
1320
  formatted_results.append(f"[{timestamp}] {content}")
1227
1321
 
1228
- return f"Found {len(results)} conversations matching '{query}':\n" + "\n".join(formatted_results)
1322
+ return f"Found {len(results)} conversations matching '{query}'s:\n" + "\n".join(formatted_results)
1229
1323
 
1230
1324
  def search_my_memories(self, query: str, limit: int = 10) -> str:
1231
1325
  """Search through this NPC's knowledge graph memories for relevant facts and concepts"""
@@ -1621,23 +1715,29 @@ class NPC:
1621
1715
  "compressed_state": self.compress_planning_state(planning_state),
1622
1716
  "summary": f"Completed {len(planning_state['successes'])} tasks for goal: {user_goal}"
1623
1717
  }
1624
-
1625
- def execute_jinx(self, jinx_name, inputs, conversation_id=None, message_id=None, team_name=None):
1626
- """Execute a jinx by name"""
1627
1718
 
1719
+ def execute_jinx(
1720
+ self,
1721
+ jinx_name,
1722
+ inputs,
1723
+ conversation_id=None,
1724
+ message_id=None,
1725
+ team_name=None,
1726
+ extra_globals=None
1727
+ ):
1628
1728
  if jinx_name in self.jinxs_dict:
1629
1729
  jinx = self.jinxs_dict[jinx_name]
1630
- elif jinx_name in self.jinxs_dict:
1631
- jinx = self.jinxs_dict[jinx_name]
1632
1730
  else:
1633
1731
  return {"error": f"jinx '{jinx_name}' not found"}
1634
1732
 
1635
1733
  result = jinx.execute(
1636
1734
  input_values=inputs,
1637
- context=self.shared_context,
1638
- jinja_env=self.jinja_env,
1639
- npc=self
1735
+ npc=self,
1736
+ # messages=messages, # messages should be passed from the calling context if available
1737
+ extra_globals=extra_globals,
1738
+ jinja_env=self.jinja_env # Pass the NPC's second-pass Jinja env
1640
1739
  )
1740
+
1641
1741
  if self.db_conn is not None:
1642
1742
  self.db_conn.add_jinx_call(
1643
1743
  triggering_message_id=message_id,
@@ -1652,7 +1752,6 @@ class NPC:
1652
1752
  team_name=team_name,
1653
1753
  )
1654
1754
  return result
1655
-
1656
1755
  def check_llm_command(self,
1657
1756
  command,
1658
1757
  messages=None,
@@ -1722,8 +1821,8 @@ class NPC:
1722
1821
  def to_dict(self):
1723
1822
  """Convert NPC to dictionary representation"""
1724
1823
  jinx_rep = []
1725
- if self.jinxs is not None:
1726
- jinx_rep = [ jinx.to_dict() if isinstance(jinx, Jinx) else jinx for jinx in self.jinxs]
1824
+ if self.jinxs_dict: # Use jinxs_dict which stores the rendered Jinx objects
1825
+ jinx_rep = [ jinx.to_dict() for jinx in self.jinxs_dict.values()]
1727
1826
  return {
1728
1827
  "name": self.name,
1729
1828
  "primary_directive": self.primary_directive,
@@ -1731,7 +1830,7 @@ class NPC:
1731
1830
  "provider": self.provider,
1732
1831
  "api_url": self.api_url,
1733
1832
  "api_key": self.api_key,
1734
- "jinxs": jinx_rep,
1833
+ "jinxs": self.jinxs_spec, # Save the original spec, not the rendered objects
1735
1834
  "use_global_jinxs": self.use_global_jinxs
1736
1835
  }
1737
1836
 
@@ -1748,10 +1847,10 @@ class NPC:
1748
1847
  def __str__(self):
1749
1848
  """String representation of NPC"""
1750
1849
  str_rep = f"NPC: {self.name}\nDirective: {self.primary_directive}\nModel: {self.model}\nProvider: {self.provider}\nAPI URL: {self.api_url}\n"
1751
- if self.jinxs:
1850
+ if self.jinxs_dict:
1752
1851
  str_rep += "Jinxs:\n"
1753
- for jinx in self.jinxs:
1754
- str_rep += f" - {jinx.jinx_name}\n"
1852
+ for jinx_name in self.jinxs_dict.keys():
1853
+ str_rep += f" - {jinx_name}\n"
1755
1854
  else:
1756
1855
  str_rep += "No jinxs available.\n"
1757
1856
  return str_rep
@@ -1769,13 +1868,11 @@ class NPC:
1769
1868
 
1770
1869
  input_values = extract_jinx_inputs(args, jinx)
1771
1870
 
1772
-
1773
-
1774
-
1775
1871
  jinx_output = jinx.execute(
1776
1872
  input_values,
1777
- jinx.jinx_name,
1778
1873
  npc=self,
1874
+ messages=messages, # Pass messages to Jinx.execute
1875
+ jinja_env=self.jinja_env # Pass the NPC's second-pass Jinja env
1779
1876
  )
1780
1877
 
1781
1878
  return {"messages": messages, "output": jinx_output}
@@ -1937,12 +2034,14 @@ class NPC:
1937
2034
  class Team:
1938
2035
  def __init__(self,
1939
2036
  team_path=None,
1940
- npcs=None,
1941
- forenpc=None,
1942
- jinxs=None,
2037
+ npcs: Optional[List['NPC']] = None, # Explicitly type hint as list of NPC
2038
+ forenpc: Optional[Union[str, 'NPC']] = None, # Can be name (str) or NPC object
2039
+ jinxs: Optional[List[Union['Jinx', Dict[str, Any]]]] = None, # List of raw Jinx objects or dicts
1943
2040
  db_conn=None,
1944
2041
  model = None,
1945
- provider = None):
2042
+ provider = None,
2043
+ api_url = None,
2044
+ api_key = None):
1946
2045
  """
1947
2046
  Initialize an NPC team from directory or list of NPCs
1948
2047
 
@@ -1953,45 +2052,249 @@ class Team:
1953
2052
  """
1954
2053
  self.model = model
1955
2054
  self.provider = provider
2055
+ self.api_url = api_url
2056
+ self.api_key = api_key
2057
+
2058
+ self.npcs: Dict[str, 'NPC'] = {} # Store NPC objects by name
2059
+ self.sub_teams: Dict[str, 'Team'] = {}
2060
+ self.jinxs_dict: Dict[str, 'Jinx'] = {} # This will store first-pass rendered Jinx objects
2061
+ self._raw_jinxs_list: List['Jinx'] = [] # Temporary storage for raw Team-level Jinx objects
1956
2062
 
1957
- self.npcs = {}
1958
- self.sub_teams = {}
1959
- self.jinxs_dict = jinxs or {}
2063
+ self.jinja_env_for_first_pass = Environment(undefined=SilentUndefined) # Env for macro expansion
2064
+
1960
2065
  self.db_conn = db_conn
1961
2066
  self.team_path = os.path.expanduser(team_path) if team_path else None
1962
2067
  self.databases = []
1963
2068
  self.mcp_servers = []
1964
- if forenpc is not None:
1965
- self.forenpc = forenpc
1966
- else:
1967
- self.forenpc = npcs[0] if npcs else None
1968
2069
 
2070
+ self.forenpc: Optional['NPC'] = None # Will be set to an NPC object by end of __init__
2071
+ self.forenpc_name: Optional[str] = None # Temporary storage for name from context (if loaded from .ctx)
2072
+
1969
2073
  if team_path:
1970
2074
  self.name = os.path.basename(os.path.abspath(team_path))
1971
- else:
2075
+ self._load_from_directory_and_initialize_forenpc()
2076
+ elif npcs:
1972
2077
  self.name = "custom_team"
2078
+ # Add provided NPCs and set their team attribute
2079
+ for npc_obj in npcs:
2080
+ self.npcs[npc_obj.name] = npc_obj
2081
+ npc_obj.team = self # Crucial: set the team for pre-existing NPCs
2082
+
2083
+ if jinxs: # Load raw team-level jinxs if provided
2084
+ for jinx_item in jinxs:
2085
+ if isinstance(jinx_item, Jinx):
2086
+ self._raw_jinxs_list.append(jinx_item)
2087
+ elif isinstance(jinx_item, dict):
2088
+ self._raw_jinxs_list.append(Jinx(jinx_data=jinx_item))
2089
+ # Assuming string jinxs are paths or names to be loaded later if needed.
2090
+
2091
+ self._determine_forenpc_from_provided_npcs(npcs, forenpc)
2092
+
2093
+ else: # No team_path and no npcs list, create a default forenpc
2094
+ self.name = "custom_team"
2095
+ self._create_default_forenpc()
2096
+
1973
2097
  self.context = ''
1974
2098
  self.shared_context = {
1975
2099
  "intermediate_results": {},
1976
2100
  "dataframes": {},
1977
2101
  "memories": {},
1978
2102
  "execution_history": [],
1979
- "npc_messages": {},
1980
2103
  "context":''
1981
2104
  }
1982
-
2105
+
2106
+ # Load team context into shared_context after forenpc is determined
2107
+ # This is for teams loaded from directory. For custom/default teams, context is set below.
1983
2108
  if team_path:
1984
- self._load_from_directory()
1985
-
1986
- elif npcs:
1987
- for npc in npcs:
1988
- self.npcs[npc.name] = npc
2109
+ self._load_team_context_into_shared_context()
2110
+ elif self.forenpc: # For custom teams or default, set basic context if not already set
2111
+ if not self.context: # Only set if context is still empty
2112
+ self.context = f"Team '{self.name}' with forenpc '{self.forenpc.name}'"
2113
+ self.shared_context['context'] = self.context
1989
2114
 
1990
- self.jinja_env = Environment(undefined=SilentUndefined)
2115
+ # Perform first-pass rendering for team-level jinxs
2116
+ self._perform_first_pass_jinx_rendering()
2117
+
2118
+ # Now, initialize jinxs for all NPCs, as team-level jinxs are ready
2119
+ for npc_obj in self.npcs.values():
2120
+ # Pass the team's raw jinxs to the NPC for its own first-pass rendering
2121
+ npc_obj.initialize_jinxs(team_raw_jinxs=self._raw_jinxs_list)
1991
2122
 
1992
2123
  if db_conn is not None:
1993
2124
  init_db_tables()
1994
2125
 
2126
+ def _load_from_directory_and_initialize_forenpc(self):
2127
+ """
2128
+ Consolidated method to load NPCs, team context, and resolve the forenpc.
2129
+ Ensures self.npcs is populated and self.forenpc is an NPC object.
2130
+ """
2131
+ if not os.path.exists(self.team_path):
2132
+ raise ValueError(f"Team directory not found: {self.team_path}")
2133
+
2134
+ # 1. Load all NPCs first (without initializing their jinxs yet)
2135
+ for filename in os.listdir(self.team_path):
2136
+ if filename.endswith(".npc"):
2137
+ npc_path = os.path.join(self.team_path, filename)
2138
+ # Pass 'self' to NPC constructor for team reference
2139
+ # Do NOT pass jinxs=... here, as it will be initialized later
2140
+ npc = NPC(npc_path, db_conn=self.db_conn, team=self)
2141
+ self.npcs[npc.name] = npc
2142
+
2143
+ # 2. Load team context and determine forenpc name (string)
2144
+ self._load_team_context_file() # This populates self.model, self.provider, self.forenpc_name etc.
2145
+
2146
+ # 3. Resolve and set self.forenpc (NPC object)
2147
+ if self.forenpc_name and self.forenpc_name in self.npcs:
2148
+ self.forenpc = self.npcs[self.forenpc_name]
2149
+ elif self.npcs: # Fallback to first NPC if name not found or not specified
2150
+ self.forenpc = list(self.npcs.values())[0]
2151
+ self.forenpc_name = self.forenpc.name # Update forenpc_name for consistency
2152
+ else: # No NPCs loaded, create a default forenpc
2153
+ self._create_default_forenpc()
2154
+
2155
+ # 4. Load raw Jinxs from team directory
2156
+ jinxs_dir = os.path.join(self.team_path, "jinxs")
2157
+ if os.path.exists(jinxs_dir):
2158
+ for jinx_obj in load_jinxs_from_directory(jinxs_dir):
2159
+ self._raw_jinxs_list.append(jinx_obj)
2160
+
2161
+ # 5. Load sub-teams
2162
+ self._load_sub_teams()
2163
+
2164
+ def _load_team_context_file(self) -> Dict[str, Any]:
2165
+ """Loads team context from .ctx file and updates team attributes."""
2166
+ ctx_data = {}
2167
+ for fname in os.listdir(self.team_path):
2168
+ if fname.endswith('.ctx'):
2169
+ ctx_data = load_yaml_file(os.path.join(self.team_path, fname))
2170
+ if ctx_data is not None:
2171
+ self.model = ctx_data.get('model', self.model)
2172
+ self.provider = ctx_data.get('provider', self.provider)
2173
+ self.api_url = ctx_data.get('api_url', self.api_url)
2174
+ self.env = ctx_data.get('env', self.env if hasattr(self, 'env') else None)
2175
+ self.mcp_servers = ctx_data.get('mcp_servers', [])
2176
+ self.databases = ctx_data.get('databases', [])
2177
+ self.forenpc_name = ctx_data.get('forenpc', self.forenpc_name) # Set forenpc_name (string)
2178
+ return ctx_data
2179
+ return {}
2180
+
2181
+ def _load_team_context_into_shared_context(self):
2182
+ """Loads team context into shared_context after forenpc is determined."""
2183
+ ctx_data = {}
2184
+ for fname in os.listdir(self.team_path):
2185
+ if fname.endswith('.ctx'):
2186
+ ctx_data = load_yaml_file(os.path.join(self.team_path, fname))
2187
+ if ctx_data is not None:
2188
+ self.context = ctx_data.get('context', '')
2189
+ self.shared_context['context'] = self.context
2190
+ if 'file_patterns' in ctx_data:
2191
+ file_cache = self._parse_file_patterns(ctx_data['file_patterns'])
2192
+ self.shared_context['files'] = file_cache
2193
+ if 'preferences' in ctx_data:
2194
+ self.preferences = ctx_data['preferences']
2195
+ else:
2196
+ self.preferences = []
2197
+
2198
+ for key, item in ctx_data.items():
2199
+ if key not in ['name', 'mcp_servers', 'databases', 'context', 'file_patterns', 'forenpc', 'model', 'provider', 'api_url', 'env', 'preferences']:
2200
+ self.shared_context[key] = item
2201
+ return # Only load the first .ctx file found
2202
+
2203
+ def _determine_forenpc_from_provided_npcs(self, npcs_list: List['NPC'], forenpc_arg: Optional[Union[str, 'NPC']]):
2204
+ """Determines self.forenpc when NPCs are provided directly to Team.__init__."""
2205
+ if forenpc_arg:
2206
+ if isinstance(forenpc_arg, NPC):
2207
+ self.forenpc = forenpc_arg
2208
+ self.forenpc_name = forenpc_arg.name
2209
+ elif isinstance(forenpc_arg, str) and forenpc_arg in self.npcs:
2210
+ self.forenpc = self.npcs[forenpc_arg]
2211
+ self.forenpc_name = forenpc_arg
2212
+ else:
2213
+ print(f"Warning: Specified forenpc '{forenpc_arg}' not found among provided NPCs. Falling back to first NPC.")
2214
+ if npcs_list:
2215
+ self.forenpc = npcs_list[0]
2216
+ self.forenpc_name = npcs_list[0].name
2217
+ else:
2218
+ self._create_default_forenpc()
2219
+ elif npcs_list: # Default to first NPC if no forenpc_arg
2220
+ self.forenpc = npcs_list[0]
2221
+ self.forenpc_name = npcs_list[0].name
2222
+ else: # No NPCs provided, create a default forenpc
2223
+ self._create_default_forenpc()
2224
+
2225
+ def _create_default_forenpc(self):
2226
+ """Creates a default forenpc if none can be determined."""
2227
+ forenpc_model = self.model or 'llama3.2'
2228
+ forenpc_provider = self.provider or 'ollama'
2229
+ forenpc_api_key = self.api_key
2230
+ forenpc_api_url = self.api_url
2231
+
2232
+ default_forenpc = NPC(name='forenpc',
2233
+ primary_directive="""You are the forenpc of the team, coordinating activities
2234
+ between NPCs on the team, verifying that results from
2235
+ NPCs are high quality and can help to adequately answer
2236
+ user requests.""",
2237
+ model=forenpc_model,
2238
+ provider=forenpc_provider,
2239
+ api_key=forenpc_api_key,
2240
+ api_url=forenpc_api_url,
2241
+ team=self # Pass the team to the forenpc
2242
+ )
2243
+ self.forenpc = default_forenpc
2244
+ self.forenpc_name = default_forenpc.name
2245
+ self.npcs[default_forenpc.name] = default_forenpc # Add to team's NPC list
2246
+
2247
+ def _perform_first_pass_jinx_rendering(self):
2248
+ """
2249
+ Performs the first-pass Jinja rendering on all loaded raw Jinxs.
2250
+ This expands nested Jinx calls but preserves runtime variables.
2251
+ """
2252
+ # Create Jinja globals for calling other Jinxs as macros
2253
+ jinx_macro_globals = {}
2254
+ for raw_jinx in self._raw_jinxs_list:
2255
+ def create_jinx_callable(jinx_obj_in_closure):
2256
+ def callable_jinx(**kwargs):
2257
+ # This callable will be invoked by the Jinja renderer during the first pass.
2258
+ # It needs to render the target Jinx's *raw* steps with the provided kwargs.
2259
+ temp_jinja_env = Environment(undefined=SilentUndefined)
2260
+
2261
+ rendered_target_steps = []
2262
+ for target_step in jinx_obj_in_closure._raw_steps:
2263
+ temp_rendered_step = {}
2264
+ for k, v in target_step.items():
2265
+ if isinstance(v, str):
2266
+ try:
2267
+ # Render the string, using kwargs as context.
2268
+ # SilentUndefined will ensure {{ var }} that are not in kwargs remain as is.
2269
+ temp_rendered_step[k] = temp_jinja_env.from_string(v).render(**kwargs)
2270
+ except Exception as e:
2271
+ print(f"Warning: Error in Jinx macro '{jinx_obj_in_closure.jinx_name}' rendering step field '{k}' (Team first pass): {e}")
2272
+ temp_rendered_step[k] = v
2273
+ else:
2274
+ temp_rendered_step[k] = v
2275
+ rendered_target_steps.append(temp_rendered_step)
2276
+
2277
+ # Return the YAML string representation of the rendered steps
2278
+ return yaml.dump(rendered_target_steps, default_flow_style=False)
2279
+ return callable_jinx
2280
+
2281
+ jinx_macro_globals[raw_jinx.jinx_name] = create_jinx_callable(raw_jinx)
2282
+
2283
+ self.jinja_env_for_first_pass.globals['jinxs'] = jinx_macro_globals # Make 'jinxs.jinx_name' callable
2284
+ self.jinja_env_for_first_pass.globals.update(jinx_macro_globals) # Also make 'jinx_name' callable directly
2285
+
2286
+ # Now, iterate through the raw Jinxs and perform the first-pass rendering
2287
+ for raw_jinx in self._raw_jinxs_list:
2288
+ try:
2289
+ # Pass the jinx_macro_globals to render_first_pass so it can resolve declarative calls
2290
+ raw_jinx.render_first_pass(self.jinja_env_for_first_pass, jinx_macro_globals)
2291
+ self.jinxs_dict[raw_jinx.jinx_name] = raw_jinx # Store the first-pass rendered Jinx
2292
+ except Exception as e:
2293
+ print(f"Error performing first-pass rendering for Jinx '{raw_jinx.jinx_name}': {e}")
2294
+
2295
+ self._raw_jinxs_list = [] # Clear temporary storage
2296
+
2297
+
1995
2298
  def update_context(self, messages: list):
1996
2299
  """Update team context based on recent conversation patterns"""
1997
2300
  if len(messages) < 10:
@@ -2035,77 +2338,6 @@ class Team:
2035
2338
  with open(team_ctx_path, 'w') as f:
2036
2339
  yaml.dump(ctx_data, f)
2037
2340
 
2038
- def _load_from_directory(self):
2039
- """Load team from directory"""
2040
- if not os.path.exists(self.team_path):
2041
- raise ValueError(f"Team directory not found: {self.team_path}")
2042
-
2043
- for filename in os.listdir(self.team_path):
2044
- if filename.endswith(".npc"):
2045
- npc_path = os.path.join(self.team_path, filename)
2046
- npc = NPC(npc_path, db_conn=self.db_conn)
2047
- self.npcs[npc.name] = npc
2048
-
2049
- self.context = self._load_team_context()
2050
-
2051
- jinxs_dir = os.path.join(self.team_path, "jinxs")
2052
- if os.path.exists(jinxs_dir):
2053
- for jinx in load_jinxs_from_directory(jinxs_dir):
2054
- self.jinxs_dict[jinx.jinx_name] = jinx
2055
-
2056
- self._load_sub_teams()
2057
-
2058
- def _load_team_context(self):
2059
- """Load team context from .ctx file"""
2060
- for fname in os.listdir(self.team_path):
2061
- if fname.endswith('.ctx'):
2062
- ctx_data = load_yaml_file(os.path.join(self.team_path, fname))
2063
- if ctx_data is not None:
2064
- if 'model' in ctx_data:
2065
- self.model = ctx_data['model']
2066
- else:
2067
- self.model = None
2068
- if 'provider' in ctx_data:
2069
- self.provider = ctx_data['provider']
2070
- else:
2071
- self.provider = None
2072
- if 'api_url' in ctx_data:
2073
- self.api_url = ctx_data['api_url']
2074
- else:
2075
- self.api_url = None
2076
- if 'env' in ctx_data:
2077
- self.env = ctx_data['env']
2078
- else:
2079
- self.env = None
2080
-
2081
- if 'mcp_servers' in ctx_data:
2082
- self.mcp_servers = ctx_data['mcp_servers']
2083
- else:
2084
- self.mcp_servers = []
2085
- if 'databases' in ctx_data:
2086
- self.databases = ctx_data['databases']
2087
- else:
2088
- self.databases = []
2089
-
2090
- base_context = ctx_data.get('context', '')
2091
- self.shared_context['context'] = base_context
2092
- if 'file_patterns' in ctx_data:
2093
- file_cache = self._parse_file_patterns(ctx_data['file_patterns'])
2094
- self.shared_context['files'] = file_cache
2095
- if 'preferences' in ctx_data:
2096
- self.preferences = ctx_data['preferences']
2097
- else:
2098
- self.preferences = []
2099
- if 'forenpc' in ctx_data:
2100
- self.forenpc = self.npcs[ctx_data['forenpc']]
2101
- else:
2102
- self.forenpc = self.npcs[list(self.npcs.keys())[0]] if self.npcs else None
2103
- for key, item in ctx_data.items():
2104
- if key not in ['name', 'mcp_servers', 'databases', 'context', 'file_patterns']:
2105
- self.shared_context[key] = item
2106
- return ctx_data
2107
- return {}
2108
-
2109
2341
  def _load_sub_teams(self):
2110
2342
  """Load sub-teams from subdirectories"""
2111
2343
  for item in os.listdir(self.team_path):
@@ -2119,47 +2351,14 @@ class Team:
2119
2351
  sub_team = Team(team_path=item_path, db_conn=self.db_conn)
2120
2352
  self.sub_teams[item] = sub_team
2121
2353
 
2122
- def get_forenpc(self):
2354
+ def get_forenpc(self) -> Optional['NPC']:
2123
2355
  """
2124
- Get the forenpc (coordinator) for this team.
2125
- The forenpc is set only if explicitly defined in the context.
2126
-
2356
+ Returns the forenpc (coordinator) for this team.
2357
+ This method is now primarily for external access, as self.forenpc is set in __init__.
2127
2358
  """
2128
- if isinstance(self.forenpc, NPC):
2129
- return self.forenpc
2130
- if hasattr(self, 'context') and self.context and 'forenpc' in self.context:
2131
- forenpc_ref = self.context['forenpc']
2132
-
2133
- if '{{ref(' in forenpc_ref:
2134
- match = re.search(r"{{\s*ref\('([^']+)'\)\s*}}", forenpc_ref)
2135
- if match:
2136
- forenpc_name = match.group(1)
2137
- if forenpc_name in self.npcs:
2138
- return self.npcs[forenpc_name]
2139
- elif forenpc_ref in self.npcs:
2140
- return self.npcs[forenpc_ref]
2141
- else:
2142
- forenpc_model=self.context.get('model', 'llama3.2'),
2143
- forenpc_provider=self.context.get('provider', 'ollama'),
2144
- forenpc_api_key=self.context.get('api_key', None),
2145
- forenpc_api_url=self.context.get('api_url', None)
2146
-
2147
- forenpc = NPC(name='forenpc',
2148
- primary_directive="""You are the forenpc of the team, coordinating activities
2149
- between NPCs on the team, verifying that results from
2150
- NPCs are high quality and can help to adequately answer
2151
- user requests.""",
2152
- model=forenpc_model,
2153
- provider=forenpc_provider,
2154
- api_key=forenpc_api_key,
2155
- api_url=forenpc_api_url,
2156
- )
2157
- self.forenpc = forenpc
2158
- self.npcs[forenpc.name] = forenpc
2159
- return forenpc
2160
- return None
2359
+ return self.forenpc
2161
2360
 
2162
- def get_npc(self, npc_ref):
2361
+ def get_npc(self, npc_ref: Union[str, 'NPC']) -> Optional['NPC']:
2163
2362
  """Get NPC by name or reference with hierarchical lookup capability"""
2164
2363
  if isinstance(npc_ref, NPC):
2165
2364
  return npc_ref
@@ -2181,7 +2380,7 @@ class Team:
2181
2380
 
2182
2381
  def orchestrate(self, request):
2183
2382
  """Orchestrate a request through the team"""
2184
- forenpc = self.get_forenpc()
2383
+ forenpc = self.get_forenpc() # Now guaranteed to be an NPC object
2185
2384
  if not forenpc:
2186
2385
  return {"error": "No forenpc available to coordinate the team"}
2187
2386
 
@@ -2326,7 +2525,7 @@ class Team:
2326
2525
  "name": self.name,
2327
2526
  "npcs": {name: npc.to_dict() for name, npc in self.npcs.items()},
2328
2527
  "sub_teams": {name: team.to_dict() for name, team in self.sub_teams.items()},
2329
- "jinxs": {name: jinx.to_dict() for name, jinx in self.jinxs.items()},
2528
+ "jinxs": {name: jinx.to_dict() for name, jinx in self.jinxs_dict.items()}, # Use jinxs_dict
2330
2529
  "context": getattr(self, 'context', {})
2331
2530
  }
2332
2531
 
@@ -2350,7 +2549,7 @@ class Team:
2350
2549
  jinxs_dir = os.path.join(directory, "jinxs")
2351
2550
  ensure_dirs_exist(jinxs_dir)
2352
2551
 
2353
- for jinx in self.jinxs.values():
2552
+ for jinx in self.jinxs_dict.values(): # Use jinxs_dict
2354
2553
  jinx.save(jinxs_dir)
2355
2554
 
2356
2555
  for team_name, team in self.sub_teams.items():
@@ -2435,4 +2634,4 @@ class Team:
2435
2634
  context_parts.append(content)
2436
2635
  context_parts.append("")
2437
2636
 
2438
- return "\n".join(context_parts)
2637
+ return "\n".join(context_parts)