npcpy 1.2.33__py3-none-any.whl → 1.2.35__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
@@ -11,6 +11,7 @@ import random
11
11
  from datetime import datetime
12
12
  import hashlib
13
13
  import pathlib
14
+ import sys
14
15
  import fnmatch
15
16
  import subprocess
16
17
  from typing import Any, Dict, List, Optional, Union, Callable, Tuple
@@ -19,7 +20,8 @@ from sqlalchemy import create_engine, text
19
20
  import npcpy as npy
20
21
  from npcpy.llm_funcs import DEFAULT_ACTION_SPACE
21
22
  from npcpy.tools import auto_tools
22
-
23
+ import math
24
+ import random
23
25
  from npcpy.npc_sysenv import (
24
26
  ensure_dirs_exist,
25
27
  init_db_tables,
@@ -34,6 +36,12 @@ class SilentUndefined(Undefined):
34
36
 
35
37
  import math
36
38
  from PIL import Image
39
+ from jinja2 import Environment, ChainableUndefined
40
+
41
+ class PreserveUndefined(ChainableUndefined):
42
+ """Undefined that preserves the original {{ variable }} syntax"""
43
+ def __str__(self):
44
+ return f"{{{{ {self._undefined_name} }}}}"
37
45
 
38
46
 
39
47
  def agent_pass_handler(command, extracted_data, **kwargs):
@@ -228,6 +236,7 @@ def write_yaml_file(file_path, data):
228
236
  print(f"Error writing YAML file {file_path}: {e}")
229
237
  return False
230
238
 
239
+
231
240
  class Jinx:
232
241
  '''
233
242
  Jinx represents a workflow template with Jinja-rendered steps.
@@ -251,17 +260,18 @@ class Jinx:
251
260
  self._load_from_data(jinx_data)
252
261
  else:
253
262
  raise ValueError("Either jinx_data or jinx_path must be provided")
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
-
263
+
264
+ # Keep a copy for macro expansion, but retain the executable steps by default
265
+ self._raw_steps = list(self.steps)
266
+ self.steps = list(self._raw_steps)
259
267
  def _load_from_file(self, path):
260
268
  jinx_data = load_yaml_file(path)
261
269
  if not jinx_data:
262
270
  raise ValueError(f"Failed to load jinx from {path}")
271
+ self._source_path = path
263
272
  self._load_from_data(jinx_data)
264
273
 
274
+
265
275
  def _load_from_data(self, jinx_data):
266
276
  if not jinx_data or not isinstance(jinx_data, dict):
267
277
  raise ValueError("Invalid jinx data provided")
@@ -273,7 +283,8 @@ class Jinx:
273
283
  self.inputs = jinx_data.get("inputs", [])
274
284
  self.description = jinx_data.get("description", "")
275
285
  self.npc = jinx_data.get("npc")
276
- self.steps = jinx_data.get("steps", []) # These are the raw steps initially
286
+ self.steps = jinx_data.get("steps", [])
287
+ self._source_path = jinx_data.get("_source_path", None)
277
288
 
278
289
  def render_first_pass(
279
290
  self,
@@ -301,6 +312,7 @@ class Jinx:
301
312
 
302
313
  engine_name = raw_step.get('engine')
303
314
 
315
+ # If this step references another jinx via engine, expand it
304
316
  if engine_name and engine_name in all_jinx_callables:
305
317
  step_name = raw_step.get('name', f'call_{engine_name}')
306
318
  jinx_args = {
@@ -314,31 +326,8 @@ class Jinx:
314
326
  expanded_steps = yaml.safe_load(expanded_yaml_string)
315
327
 
316
328
  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
329
  rendered_steps_output.extend(expanded_steps)
330
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
331
  elif expanded_steps is not None:
343
332
  rendered_steps_output.append(expanded_steps)
344
333
 
@@ -349,7 +338,11 @@ class Jinx:
349
338
  f"(declarative): {e}"
350
339
  )
351
340
  rendered_steps_output.append(raw_step)
341
+ # Skip rendering for python/bash engine steps - preserve runtime variables
342
+ elif raw_step.get('engine') in ['python', 'bash']:
343
+ rendered_steps_output.append(raw_step)
352
344
  else:
345
+ # For other steps, do first-pass rendering (inline macro expansion)
353
346
  processed_step = {}
354
347
  for key, value in raw_step.items():
355
348
  if isinstance(value, str):
@@ -384,9 +377,8 @@ class Jinx:
384
377
  npc: Optional[Any] = None,
385
378
  messages: Optional[List[Dict[str, str]]] = None,
386
379
  extra_globals: Optional[Dict[str, Any]] = None,
387
- jinja_env: Optional[Environment] = None): # Add jinja_env here for the second pass
380
+ jinja_env: Optional[Environment] = None):
388
381
 
389
- # If jinja_env is not provided, create a default one for the second pass
390
382
  if jinja_env is None:
391
383
  jinja_env = Environment(
392
384
  loader=DictLoader({}),
@@ -395,7 +387,11 @@ class Jinx:
395
387
 
396
388
  active_npc = self.npc if self.npc else npc
397
389
 
398
- context = (active_npc.shared_context.copy() if active_npc and hasattr(active_npc, 'shared_context') else {})
390
+ context = (
391
+ active_npc.shared_context.copy()
392
+ if active_npc and hasattr(active_npc, 'shared_context')
393
+ else {}
394
+ )
399
395
  context.update(input_values)
400
396
  context.update({
401
397
  "llm_response": None,
@@ -404,12 +400,11 @@ class Jinx:
404
400
  "npc": active_npc
405
401
  })
406
402
 
407
- # Iterate over self.steps, which are now first-pass rendered
408
403
  for i, step in enumerate(self.steps):
409
404
  context = self._execute_step(
410
405
  step,
411
406
  context,
412
- jinja_env, # This is the second-pass Jinja env
407
+ jinja_env,
413
408
  npc=active_npc,
414
409
  messages=messages,
415
410
  extra_globals=extra_globals
@@ -420,31 +415,53 @@ class Jinx:
420
415
  def _execute_step(self,
421
416
  step: Dict[str, Any],
422
417
  context: Dict[str, Any],
423
- jinja_env: Environment, # This is for the second pass (runtime vars)
418
+ jinja_env: Environment,
424
419
  npc: Optional[Any] = None,
425
420
  messages: Optional[List[Dict[str, str]]] = None,
426
421
  extra_globals: Optional[Dict[str, Any]] = None):
427
422
 
428
- code_content = step.get("code", "") # Get the code content, which might still have {{ runtime_var }}
423
+ def _log_debug(msg):
424
+ log_file_path = os.path.expanduser("~/jinx_debug_log.txt")
425
+ with open(log_file_path, "a") as f:
426
+ f.write(f"[{datetime.now().isoformat()}] {msg}\n")
427
+
428
+ code_content = step.get("code", "")
429
429
  step_name = step.get("name", "unnamed_step")
430
430
  step_npc = step.get("npc")
431
431
 
432
432
  active_npc = step_npc if step_npc else npc
433
433
 
434
- # Second pass rendering: resolve runtime variables in the code content
435
434
  try:
436
435
  template = jinja_env.from_string(code_content)
437
436
  rendered_code = template.render(**context)
438
437
  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
438
+ _log_debug(
439
+ f"Error rendering template for step {step_name} "
440
+ f"(second pass): {e}"
441
+ )
442
+ rendered_code = code_content
441
443
 
444
+ _log_debug(f"rendered jinx code: {rendered_code}")
445
+ _log_debug(
446
+ f"DEBUG: Before exec - rendered_code: {rendered_code}"
447
+ )
448
+ _log_debug(
449
+ f"DEBUG: Before exec - context['output'] before step: "
450
+ f"{context.get('output')}"
451
+ )
452
+
442
453
  exec_globals = {
443
454
  "__builtins__": __builtins__,
444
455
  "npc": active_npc,
445
456
  "context": context,
457
+ "math": math,
458
+ "random": random,
459
+ "datetime": datetime,
460
+ "Image": Image,
446
461
  "pd": pd,
447
462
  "plt": plt,
463
+ "sys": sys,
464
+ "subprocess": subprocess,
448
465
  "np": np,
449
466
  "os": os,
450
467
  're': re,
@@ -465,33 +482,56 @@ class Jinx:
465
482
  try:
466
483
  exec(rendered_code, exec_globals, exec_locals)
467
484
  except Exception as e:
468
- error_msg = f"Error executing step {step_name}: {type(e).__name__}: {e}"
485
+ error_msg = (
486
+ f"Error executing step {step_name}: "
487
+ f"{type(e).__name__}: {e}"
488
+ )
469
489
  context['output'] = error_msg
470
- print(error_msg)
490
+ _log_debug(error_msg)
471
491
  return context
472
492
 
493
+ _log_debug(f"DEBUG: After exec - exec_locals: {exec_locals}")
494
+ _log_debug(
495
+ f"DEBUG: After exec - 'output' in exec_locals: "
496
+ f"{'output' in exec_locals}"
497
+ )
498
+
473
499
  context.update(exec_locals)
474
500
 
501
+ _log_debug(
502
+ f"DEBUG: After context.update(exec_locals) - "
503
+ f"context['output']: {context.get('output')}"
504
+ )
505
+ _log_debug(f"context after jinx ex: {context}")
506
+
475
507
  if "output" in exec_locals:
476
508
  outp = exec_locals["output"]
477
509
  context["output"] = outp
478
510
  context[step_name] = outp
511
+
512
+ _log_debug(
513
+ f"DEBUG: Inside 'output' in exec_locals block - "
514
+ f"context['output']: {context.get('output')}"
515
+ )
516
+
479
517
  if messages is not None:
480
518
  messages.append({
481
519
  'role':'assistant',
482
- 'content': f'Jinx {self.jinx_name} step {step_name} executed: {outp}'
520
+ 'content': (
521
+ f'Jinx {self.jinx_name} step {step_name} '
522
+ f'executed: {outp}'
523
+ )
483
524
  })
484
525
  context['messages'] = messages
485
526
 
486
527
  return context
528
+
487
529
 
488
530
  def to_dict(self):
489
531
  result = {
490
532
  "jinx_name": self.jinx_name,
491
533
  "description": self.description,
492
534
  "inputs": self.inputs,
493
- # When converting to dict, we should save the *raw* steps
494
- # so that when reloaded, the first pass can be done again.
495
535
  "steps": self._raw_steps
496
536
  }
497
537
 
@@ -517,8 +557,16 @@ class Jinx:
517
557
  inputs = []
518
558
  for param_name, param in signature.parameters.items():
519
559
  if param_name != 'self':
520
- param_type = param.annotation if param.annotation != inspect.Parameter.empty else None
521
- param_default = None if param.default == inspect.Parameter.empty else param.default
560
+ param_type = (
561
+ param.annotation
562
+ if param.annotation != inspect.Parameter.empty
563
+ else None
564
+ )
565
+ param_default = (
566
+ None
567
+ if param.default == inspect.Parameter.empty
568
+ else param.default
569
+ )
522
570
 
523
571
  inputs.append({
524
572
  "name": param_name,
@@ -536,7 +584,10 @@ class Jinx:
536
584
  "code": f"""
537
585
  import {mcp_tool.__module__}
538
586
  output = {mcp_tool.__module__}.{name}(
539
- {', '.join([f'{inp["name"]}=context.get("{inp["name"]}")' for inp in inputs])}
587
+ {', '.join([
588
+ f'{inp["name"]}=context.get("{inp["name"]}")'
589
+ for inp in inputs
590
+ ])}
540
591
  )
541
592
  """
542
593
  }
@@ -575,8 +626,9 @@ def get_npc_action_space(npc=None, team=None):
575
626
  if npc:
576
627
  core_tools = [
577
628
  npc.think_step_by_step,
578
- npc.write_code
579
629
  ]
630
+ if hasattr(npc, "write_code"):
631
+ core_tools.append(npc.write_code)
580
632
 
581
633
  if npc.command_history:
582
634
  core_tools.extend([
@@ -1392,113 +1444,6 @@ class NPC:
1392
1444
  response = self.get_llm_response(thinking_prompt, tool_choice = False)
1393
1445
  return response.get('response', 'Unable to process thinking request')
1394
1446
 
1395
- def write_code(self, task_description: str, language: str = "python", show=True) -> str:
1396
- """Generate and execute code for a specific task, returning the result"""
1397
- if language.lower() != "python":
1398
-
1399
- code_prompt = f"""Write {language} code for the following task:
1400
- {task_description}
1401
-
1402
- Provide clean, working code with brief explanations for key parts:"""
1403
-
1404
- response = self.get_llm_response(code_prompt, tool_choice=False )
1405
- return response.get('response', 'Unable to generate code')
1406
-
1407
-
1408
- code_prompt = f"""Write Python code for the following task:
1409
- {task_description}
1410
-
1411
- Requirements:
1412
- - Provide executable Python code
1413
- - Store the final result in a variable called 'output'
1414
- - Include any necessary imports
1415
- - Handle errors gracefully
1416
- - The code should be ready to execute without modification
1417
-
1418
- Example format:
1419
- ```python
1420
- import pandas as pd
1421
- # Your code here
1422
- result = some_calculation()
1423
- output = f"Task completed successfully: {{result}}"
1424
- """
1425
- response = self.get_llm_response(code_prompt, tool_choice= False)
1426
- generated_code = response.get('response', '')
1427
-
1428
-
1429
- if '```python' in generated_code:
1430
- code_lines = generated_code.split('\n')
1431
- start_idx = None
1432
- end_idx = None
1433
-
1434
- for i, line in enumerate(code_lines):
1435
- if '```python' in line:
1436
- start_idx = i + 1
1437
- elif '```' in line and start_idx is not None:
1438
- end_idx = i
1439
- break
1440
-
1441
- if start_idx is not None:
1442
- if end_idx is not None:
1443
- generated_code = '\n'.join(code_lines[start_idx:end_idx])
1444
- else:
1445
- generated_code = '\n'.join(code_lines[start_idx:])
1446
-
1447
- try:
1448
-
1449
- exec_globals = {
1450
- "__builtins__": __builtins__,
1451
- "npc": self,
1452
- "context": self.shared_context,
1453
- "pd": pd,
1454
- "plt": plt,
1455
- "np": np,
1456
- "os": os,
1457
- "re": re,
1458
- "json": json,
1459
- "Path": pathlib.Path,
1460
- "fnmatch": fnmatch,
1461
- "pathlib": pathlib,
1462
- "subprocess": subprocess,
1463
- "datetime": datetime,
1464
- "hashlib": hashlib,
1465
- "sqlite3": sqlite3,
1466
- "yaml": yaml,
1467
- "random": random,
1468
- "math": math,
1469
- }
1470
-
1471
- exec_locals = {}
1472
-
1473
-
1474
- exec(generated_code, exec_globals, exec_locals)
1475
-
1476
- if show:
1477
- print('Executing code', generated_code)
1478
-
1479
-
1480
- if "output" in exec_locals:
1481
- result = exec_locals["output"]
1482
-
1483
- self.shared_context.update({k: v for k, v in exec_locals.items()
1484
- if not k.startswith('_') and not callable(v)})
1485
- return f"Code executed successfully. Result: {result}"
1486
- else:
1487
-
1488
- meaningful_vars = {k: v for k, v in exec_locals.items()
1489
- if not k.startswith('_') and not callable(v)}
1490
-
1491
- self.shared_context.update(meaningful_vars)
1492
-
1493
- if meaningful_vars:
1494
- last_var = list(meaningful_vars.items())[-1]
1495
- return f"Code executed successfully. Last result: {last_var[0]} = {last_var[1]}"
1496
- else:
1497
- return "Code executed successfully (no explicit output generated)"
1498
-
1499
- except Exception as e:
1500
- error_msg = f"Error executing code: {str(e)}\n\nGenerated code was:\n{generated_code}"
1501
- return error_msg
1502
1447
 
1503
1448
 
1504
1449
 
@@ -2634,4 +2579,4 @@ class Team:
2634
2579
  context_parts.append(content)
2635
2580
  context_parts.append("")
2636
2581
 
2637
- return "\n".join(context_parts)
2582
+ return "\n".join(context_parts)