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/data/audio.py +35 -1
- npcpy/data/load.py +149 -7
- npcpy/data/video.py +72 -0
- npcpy/ft/diff.py +332 -71
- npcpy/gen/image_gen.py +120 -23
- npcpy/gen/ocr.py +187 -0
- npcpy/memory/command_history.py +257 -41
- npcpy/npc_compiler.py +102 -157
- npcpy/serve.py +1469 -739
- {npcpy-1.2.33.dist-info → npcpy-1.2.35.dist-info}/METADATA +1 -1
- {npcpy-1.2.33.dist-info → npcpy-1.2.35.dist-info}/RECORD +14 -13
- {npcpy-1.2.33.dist-info → npcpy-1.2.35.dist-info}/WHEEL +0 -0
- {npcpy-1.2.33.dist-info → npcpy-1.2.35.dist-info}/licenses/LICENSE +0 -0
- {npcpy-1.2.33.dist-info → npcpy-1.2.35.dist-info}/top_level.txt +0 -0
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
|
-
#
|
|
256
|
-
self._raw_steps = list(self.steps)
|
|
257
|
-
self.steps =
|
|
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", [])
|
|
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):
|
|
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 = (
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
440
|
-
|
|
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 =
|
|
485
|
+
error_msg = (
|
|
486
|
+
f"Error executing step {step_name}: "
|
|
487
|
+
f"{type(e).__name__}: {e}"
|
|
488
|
+
)
|
|
469
489
|
context['output'] = error_msg
|
|
470
|
-
|
|
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':
|
|
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 =
|
|
521
|
-
|
|
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([
|
|
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)
|