npcpy 1.2.32__py3-none-any.whl → 1.2.34__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/llm_funcs.py +57 -37
- npcpy/memory/command_history.py +26 -1
- npcpy/npc_compiler.py +591 -456
- npcpy/serve.py +362 -291
- {npcpy-1.2.32.dist-info → npcpy-1.2.34.dist-info}/METADATA +97 -34
- {npcpy-1.2.32.dist-info → npcpy-1.2.34.dist-info}/RECORD +9 -9
- {npcpy-1.2.32.dist-info → npcpy-1.2.34.dist-info}/WHEEL +0 -0
- {npcpy-1.2.32.dist-info → npcpy-1.2.34.dist-info}/licenses/LICENSE +0 -0
- {npcpy-1.2.32.dist-info → npcpy-1.2.34.dist-info}/top_level.txt +0 -0
npcpy/npc_compiler.py
CHANGED
|
@@ -11,10 +11,11 @@ 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
|
-
from typing import Any, Dict, List, Optional, Union
|
|
17
|
-
from jinja2 import Environment, FileSystemLoader, Template, Undefined
|
|
17
|
+
from typing import Any, Dict, List, Optional, Union, Callable, Tuple
|
|
18
|
+
from jinja2 import Environment, FileSystemLoader, Template, Undefined, DictLoader
|
|
18
19
|
from sqlalchemy import create_engine, text
|
|
19
20
|
import npcpy as npy
|
|
20
21
|
from npcpy.llm_funcs import DEFAULT_ACTION_SPACE
|
|
@@ -34,6 +35,12 @@ class SilentUndefined(Undefined):
|
|
|
34
35
|
|
|
35
36
|
import math
|
|
36
37
|
from PIL import Image
|
|
38
|
+
from jinja2 import Environment, ChainableUndefined
|
|
39
|
+
|
|
40
|
+
class PreserveUndefined(ChainableUndefined):
|
|
41
|
+
"""Undefined that preserves the original {{ variable }} syntax"""
|
|
42
|
+
def __str__(self):
|
|
43
|
+
return f"{{{{ {self._undefined_name} }}}}"
|
|
37
44
|
|
|
38
45
|
|
|
39
46
|
def agent_pass_handler(command, extracted_data, **kwargs):
|
|
@@ -231,13 +238,21 @@ def write_yaml_file(file_path, data):
|
|
|
231
238
|
|
|
232
239
|
class Jinx:
|
|
233
240
|
'''
|
|
241
|
+
Jinx represents a workflow template with Jinja-rendered steps.
|
|
242
|
+
|
|
243
|
+
Loads YAML definition containing:
|
|
244
|
+
- jinx_name: identifier
|
|
245
|
+
- inputs: list of input parameters
|
|
246
|
+
- description: what the jinx does
|
|
247
|
+
- npc: optional NPC to execute with
|
|
248
|
+
- steps: list of step definitions with code
|
|
234
249
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
250
|
+
Execution:
|
|
251
|
+
- Renders Jinja templates in step code with input values
|
|
252
|
+
- Executes resulting Python code
|
|
253
|
+
- Returns context with outputs
|
|
238
254
|
'''
|
|
239
255
|
def __init__(self, jinx_data=None, jinx_path=None):
|
|
240
|
-
"""Initialize a jinx from data or file path"""
|
|
241
256
|
if jinx_path:
|
|
242
257
|
self._load_from_file(jinx_path)
|
|
243
258
|
elif jinx_data:
|
|
@@ -245,15 +260,16 @@ class Jinx:
|
|
|
245
260
|
else:
|
|
246
261
|
raise ValueError("Either jinx_data or jinx_path must be provided")
|
|
247
262
|
|
|
263
|
+
self._raw_steps = list(self.steps)
|
|
264
|
+
self.steps = []
|
|
265
|
+
|
|
248
266
|
def _load_from_file(self, path):
|
|
249
|
-
"""Load jinx from file"""
|
|
250
267
|
jinx_data = load_yaml_file(path)
|
|
251
268
|
if not jinx_data:
|
|
252
269
|
raise ValueError(f"Failed to load jinx from {path}")
|
|
253
270
|
self._load_from_data(jinx_data)
|
|
254
271
|
|
|
255
272
|
def _load_from_data(self, jinx_data):
|
|
256
|
-
"""Load jinx from data dictionary"""
|
|
257
273
|
if not jinx_data or not isinstance(jinx_data, dict):
|
|
258
274
|
raise ValueError("Invalid jinx data provided")
|
|
259
275
|
|
|
@@ -263,62 +279,131 @@ class Jinx:
|
|
|
263
279
|
self.jinx_name = jinx_data.get("jinx_name")
|
|
264
280
|
self.inputs = jinx_data.get("inputs", [])
|
|
265
281
|
self.description = jinx_data.get("description", "")
|
|
266
|
-
self.
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
282
|
+
self.npc = jinx_data.get("npc")
|
|
283
|
+
self.steps = jinx_data.get("steps", [])
|
|
284
|
+
|
|
285
|
+
def render_first_pass(
|
|
286
|
+
self,
|
|
287
|
+
jinja_env_for_macros: Environment,
|
|
288
|
+
all_jinx_callables: Dict[str, Callable]
|
|
289
|
+
):
|
|
290
|
+
"""
|
|
291
|
+
Performs the first-pass Jinja rendering on the Jinx's raw steps.
|
|
292
|
+
This expands nested Jinx calls (e.g., {{ sh(...) }} or
|
|
293
|
+
engine: jinx_name) but preserves runtime variables
|
|
294
|
+
(e.g., {{ command_var }}).
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
jinja_env_for_macros: The Jinja Environment configured with
|
|
298
|
+
Jinx callables in its globals.
|
|
299
|
+
all_jinx_callables: A dictionary of Jinx names to their
|
|
300
|
+
callable functions (from create_jinx_callable).
|
|
301
|
+
"""
|
|
302
|
+
rendered_steps_output = []
|
|
303
|
+
|
|
304
|
+
for raw_step in self._raw_steps:
|
|
305
|
+
if not isinstance(raw_step, dict):
|
|
306
|
+
rendered_steps_output.append(raw_step)
|
|
307
|
+
continue
|
|
308
|
+
|
|
309
|
+
engine_name = raw_step.get('engine')
|
|
310
|
+
|
|
311
|
+
# If this step references another jinx via engine, expand it
|
|
312
|
+
if engine_name and engine_name in all_jinx_callables:
|
|
313
|
+
step_name = raw_step.get('name', f'call_{engine_name}')
|
|
314
|
+
jinx_args = {
|
|
315
|
+
k: v for k, v in raw_step.items()
|
|
316
|
+
if k not in ['engine', 'name']
|
|
276
317
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
318
|
+
|
|
319
|
+
jinx_callable = all_jinx_callables[engine_name]
|
|
320
|
+
try:
|
|
321
|
+
expanded_yaml_string = jinx_callable(**jinx_args)
|
|
322
|
+
expanded_steps = yaml.safe_load(expanded_yaml_string)
|
|
323
|
+
|
|
324
|
+
if isinstance(expanded_steps, list):
|
|
325
|
+
rendered_steps_output.extend(expanded_steps)
|
|
326
|
+
|
|
327
|
+
elif expanded_steps is not None:
|
|
328
|
+
rendered_steps_output.append(expanded_steps)
|
|
329
|
+
|
|
330
|
+
except Exception as e:
|
|
331
|
+
print(
|
|
332
|
+
f"Warning: Error expanding Jinx '{engine_name}' "
|
|
333
|
+
f"within Jinx '{self.jinx_name}' "
|
|
334
|
+
f"(declarative): {e}"
|
|
335
|
+
)
|
|
336
|
+
rendered_steps_output.append(raw_step)
|
|
337
|
+
# Skip rendering for python/bash engine steps - preserve runtime variables
|
|
338
|
+
elif raw_step.get('engine') in ['python', 'bash']:
|
|
339
|
+
rendered_steps_output.append(raw_step)
|
|
280
340
|
else:
|
|
281
|
-
|
|
282
|
-
|
|
341
|
+
# For other steps, do first-pass rendering (inline macro expansion)
|
|
342
|
+
processed_step = {}
|
|
343
|
+
for key, value in raw_step.items():
|
|
344
|
+
if isinstance(value, str):
|
|
345
|
+
try:
|
|
346
|
+
template = jinja_env_for_macros.from_string(
|
|
347
|
+
value
|
|
348
|
+
)
|
|
349
|
+
rendered_value = template.render({})
|
|
350
|
+
|
|
351
|
+
try:
|
|
352
|
+
loaded_value = yaml.safe_load(
|
|
353
|
+
rendered_value
|
|
354
|
+
)
|
|
355
|
+
processed_step[key] = loaded_value
|
|
356
|
+
except yaml.YAMLError:
|
|
357
|
+
processed_step[key] = rendered_value
|
|
358
|
+
except Exception as e:
|
|
359
|
+
print(
|
|
360
|
+
f"Warning: Error during first-pass "
|
|
361
|
+
f"rendering of Jinx '{self.jinx_name}' "
|
|
362
|
+
f"step field '{key}' (inline macro): {e}"
|
|
363
|
+
)
|
|
364
|
+
processed_step[key] = value
|
|
365
|
+
else:
|
|
366
|
+
processed_step[key] = value
|
|
367
|
+
rendered_steps_output.append(processed_step)
|
|
368
|
+
|
|
369
|
+
self.steps = rendered_steps_output
|
|
283
370
|
|
|
284
371
|
def execute(self,
|
|
285
372
|
input_values: Dict[str, Any],
|
|
286
|
-
jinxs_dict: Dict[str, 'Jinx'],
|
|
287
|
-
jinja_env: Optional[Environment] = None,
|
|
288
373
|
npc: Optional[Any] = None,
|
|
289
374
|
messages: Optional[List[Dict[str, str]]] = None,
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
**kwargs can be used to pass 'extra_globals' for the python engine.
|
|
294
|
-
"""
|
|
375
|
+
extra_globals: Optional[Dict[str, Any]] = None,
|
|
376
|
+
jinja_env: Optional[Environment] = None):
|
|
377
|
+
|
|
295
378
|
if jinja_env is None:
|
|
296
|
-
from jinja2 import DictLoader
|
|
297
379
|
jinja_env = Environment(
|
|
298
380
|
loader=DictLoader({}),
|
|
299
381
|
undefined=SilentUndefined,
|
|
300
382
|
)
|
|
301
383
|
|
|
302
|
-
|
|
384
|
+
active_npc = self.npc if self.npc else npc
|
|
385
|
+
|
|
386
|
+
context = (
|
|
387
|
+
active_npc.shared_context.copy()
|
|
388
|
+
if active_npc and hasattr(active_npc, 'shared_context')
|
|
389
|
+
else {}
|
|
390
|
+
)
|
|
303
391
|
context.update(input_values)
|
|
304
392
|
context.update({
|
|
305
|
-
"jinxs": jinxs_dict,
|
|
306
393
|
"llm_response": None,
|
|
307
394
|
"output": None,
|
|
308
395
|
"messages": messages,
|
|
396
|
+
"npc": active_npc
|
|
309
397
|
})
|
|
310
|
-
|
|
311
|
-
# This is the key change: Extract 'extra_globals' from kwargs
|
|
312
|
-
extra_globals = kwargs.get('extra_globals')
|
|
313
398
|
|
|
314
399
|
for i, step in enumerate(self.steps):
|
|
315
400
|
context = self._execute_step(
|
|
316
401
|
step,
|
|
317
402
|
context,
|
|
318
403
|
jinja_env,
|
|
319
|
-
npc=
|
|
404
|
+
npc=active_npc,
|
|
320
405
|
messages=messages,
|
|
321
|
-
extra_globals=extra_globals
|
|
406
|
+
extra_globals=extra_globals
|
|
322
407
|
)
|
|
323
408
|
|
|
324
409
|
return context
|
|
@@ -330,141 +415,150 @@ class Jinx:
|
|
|
330
415
|
npc: Optional[Any] = None,
|
|
331
416
|
messages: Optional[List[Dict[str, str]]] = None,
|
|
332
417
|
extra_globals: Optional[Dict[str, Any]] = None):
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
step_name = step.get("name", "unnamed_step")
|
|
339
|
-
mode = step.get("mode", "chat")
|
|
418
|
+
|
|
419
|
+
def _log_debug(msg):
|
|
420
|
+
log_file_path = os.path.expanduser("~/jinx_debug_log.txt")
|
|
421
|
+
with open(log_file_path, "a") as f:
|
|
422
|
+
f.write(f"[{datetime.now().isoformat()}] {msg}\n")
|
|
340
423
|
|
|
424
|
+
code_content = step.get("code", "")
|
|
425
|
+
step_name = step.get("name", "unnamed_step")
|
|
426
|
+
step_npc = step.get("npc")
|
|
427
|
+
|
|
428
|
+
active_npc = step_npc if step_npc else npc
|
|
429
|
+
|
|
341
430
|
try:
|
|
342
|
-
template = jinja_env.from_string(
|
|
431
|
+
template = jinja_env.from_string(code_content)
|
|
343
432
|
rendered_code = template.render(**context)
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
433
|
+
except Exception as e:
|
|
434
|
+
_log_debug(
|
|
435
|
+
f"Error rendering template for step {step_name} "
|
|
436
|
+
f"(second pass): {e}"
|
|
437
|
+
)
|
|
438
|
+
rendered_code = code_content
|
|
439
|
+
|
|
440
|
+
_log_debug(f"rendered jinx code: {rendered_code}")
|
|
441
|
+
_log_debug(
|
|
442
|
+
f"DEBUG: Before exec - rendered_code: {rendered_code}"
|
|
443
|
+
)
|
|
444
|
+
_log_debug(
|
|
445
|
+
f"DEBUG: Before exec - context['output'] before step: "
|
|
446
|
+
f"{context.get('output')}"
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
exec_globals = {
|
|
450
|
+
"__builtins__": __builtins__,
|
|
451
|
+
"npc": active_npc,
|
|
452
|
+
"context": context,
|
|
453
|
+
"pd": pd,
|
|
454
|
+
"plt": plt,
|
|
455
|
+
"sys": sys,
|
|
456
|
+
"subprocess": subprocess,
|
|
457
|
+
"np": np,
|
|
458
|
+
"os": os,
|
|
459
|
+
're': re,
|
|
460
|
+
"json": json,
|
|
461
|
+
"Path": pathlib.Path,
|
|
462
|
+
"fnmatch": fnmatch,
|
|
463
|
+
"pathlib": pathlib,
|
|
464
|
+
"subprocess": subprocess,
|
|
465
|
+
"get_llm_response": npy.llm_funcs.get_llm_response,
|
|
466
|
+
"CommandHistory": CommandHistory,
|
|
467
|
+
}
|
|
347
468
|
|
|
469
|
+
if extra_globals:
|
|
470
|
+
exec_globals.update(extra_globals)
|
|
471
|
+
|
|
472
|
+
exec_locals = {}
|
|
473
|
+
|
|
474
|
+
try:
|
|
475
|
+
exec(rendered_code, exec_globals, exec_locals)
|
|
348
476
|
except Exception as e:
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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')
|
|
376
|
-
|
|
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
|
|
477
|
+
error_msg = (
|
|
478
|
+
f"Error executing step {step_name}: "
|
|
479
|
+
f"{type(e).__name__}: {e}"
|
|
480
|
+
)
|
|
481
|
+
context['output'] = error_msg
|
|
482
|
+
_log_debug(error_msg)
|
|
483
|
+
return context
|
|
484
|
+
|
|
485
|
+
_log_debug(f"DEBUG: After exec - exec_locals: {exec_locals}")
|
|
486
|
+
_log_debug(
|
|
487
|
+
f"DEBUG: After exec - 'output' in exec_locals: "
|
|
488
|
+
f"{'output' in exec_locals}"
|
|
489
|
+
)
|
|
409
490
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
491
|
+
context.update(exec_locals)
|
|
492
|
+
|
|
493
|
+
_log_debug(
|
|
494
|
+
f"DEBUG: After context.update(exec_locals) - "
|
|
495
|
+
f"context['output']: {context.get('output')}"
|
|
496
|
+
)
|
|
497
|
+
_log_debug(f"context after jinx ex: {context}")
|
|
498
|
+
|
|
499
|
+
if "output" in exec_locals:
|
|
500
|
+
outp = exec_locals["output"]
|
|
501
|
+
context["output"] = outp
|
|
502
|
+
context[step_name] = outp
|
|
503
|
+
|
|
504
|
+
_log_debug(
|
|
505
|
+
f"DEBUG: Inside 'output' in exec_locals block - "
|
|
506
|
+
f"context['output']: {context.get('output')}"
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
if messages is not None:
|
|
510
|
+
messages.append({
|
|
511
|
+
'role':'assistant',
|
|
512
|
+
'content': (
|
|
513
|
+
f'Jinx {self.jinx_name} step {step_name} '
|
|
514
|
+
f'executed: {outp}'
|
|
515
|
+
)
|
|
516
|
+
})
|
|
517
|
+
context['messages'] = messages
|
|
518
|
+
|
|
424
519
|
return context
|
|
425
|
-
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
520
|
|
|
438
|
-
|
|
521
|
+
|
|
522
|
+
def to_dict(self):
|
|
523
|
+
result = {
|
|
439
524
|
"jinx_name": self.jinx_name,
|
|
440
525
|
"description": self.description,
|
|
441
526
|
"inputs": self.inputs,
|
|
442
|
-
"steps":
|
|
527
|
+
"steps": self._raw_steps
|
|
443
528
|
}
|
|
529
|
+
|
|
530
|
+
if self.npc:
|
|
531
|
+
result["npc"] = self.npc
|
|
532
|
+
|
|
533
|
+
return result
|
|
534
|
+
|
|
444
535
|
def save(self, directory):
|
|
445
|
-
"""Save jinx to file"""
|
|
446
536
|
jinx_path = os.path.join(directory, f"{self.jinx_name}.jinx")
|
|
447
537
|
ensure_dirs_exist(os.path.dirname(jinx_path))
|
|
448
538
|
return write_yaml_file(jinx_path, self.to_dict())
|
|
449
539
|
|
|
450
540
|
@classmethod
|
|
451
541
|
def from_mcp(cls, mcp_tool):
|
|
452
|
-
"""Convert an MCP tool to NPC jinx format"""
|
|
453
|
-
|
|
454
542
|
try:
|
|
455
543
|
import inspect
|
|
456
544
|
|
|
457
|
-
|
|
458
545
|
doc = mcp_tool.__doc__ or ""
|
|
459
546
|
name = mcp_tool.__name__
|
|
460
547
|
signature = inspect.signature(mcp_tool)
|
|
461
548
|
|
|
462
|
-
|
|
463
549
|
inputs = []
|
|
464
550
|
for param_name, param in signature.parameters.items():
|
|
465
|
-
if param_name != 'self':
|
|
466
|
-
param_type =
|
|
467
|
-
|
|
551
|
+
if param_name != 'self':
|
|
552
|
+
param_type = (
|
|
553
|
+
param.annotation
|
|
554
|
+
if param.annotation != inspect.Parameter.empty
|
|
555
|
+
else None
|
|
556
|
+
)
|
|
557
|
+
param_default = (
|
|
558
|
+
None
|
|
559
|
+
if param.default == inspect.Parameter.empty
|
|
560
|
+
else param.default
|
|
561
|
+
)
|
|
468
562
|
|
|
469
563
|
inputs.append({
|
|
470
564
|
"name": param_name,
|
|
@@ -472,7 +566,6 @@ class Jinx:
|
|
|
472
566
|
"default": param_default
|
|
473
567
|
})
|
|
474
568
|
|
|
475
|
-
|
|
476
569
|
jinx_data = {
|
|
477
570
|
"jinx_name": name,
|
|
478
571
|
"description": doc.strip(),
|
|
@@ -480,12 +573,13 @@ class Jinx:
|
|
|
480
573
|
"steps": [
|
|
481
574
|
{
|
|
482
575
|
"name": "mcp_function_call",
|
|
483
|
-
"engine": "python",
|
|
484
576
|
"code": f"""
|
|
485
|
-
|
|
486
577
|
import {mcp_tool.__module__}
|
|
487
578
|
output = {mcp_tool.__module__}.{name}(
|
|
488
|
-
{', '.join([
|
|
579
|
+
{', '.join([
|
|
580
|
+
f'{inp["name"]}=context.get("{inp["name"]}")'
|
|
581
|
+
for inp in inputs
|
|
582
|
+
])}
|
|
489
583
|
)
|
|
490
584
|
"""
|
|
491
585
|
}
|
|
@@ -495,7 +589,7 @@ output = {mcp_tool.__module__}.{name}(
|
|
|
495
589
|
return cls(jinx_data=jinx_data)
|
|
496
590
|
|
|
497
591
|
except:
|
|
498
|
-
pass
|
|
592
|
+
pass
|
|
499
593
|
|
|
500
594
|
def load_jinxs_from_directory(directory):
|
|
501
595
|
"""Load all jinxs from a directory recursively"""
|
|
@@ -687,8 +781,8 @@ class NPC:
|
|
|
687
781
|
name: str = None,
|
|
688
782
|
primary_directive: str = None,
|
|
689
783
|
plain_system_message: bool = False,
|
|
690
|
-
team = None,
|
|
691
|
-
jinxs: list = None,
|
|
784
|
+
team = None, # Can be None initially
|
|
785
|
+
jinxs: list = None, # Explicit jinxs for this NPC
|
|
692
786
|
tools: list = None,
|
|
693
787
|
model: str = None,
|
|
694
788
|
provider: str = None,
|
|
@@ -696,7 +790,7 @@ class NPC:
|
|
|
696
790
|
api_key: str = None,
|
|
697
791
|
db_conn=None,
|
|
698
792
|
use_global_jinxs=False,
|
|
699
|
-
memory = False,
|
|
793
|
+
memory = False,
|
|
700
794
|
**kwargs
|
|
701
795
|
):
|
|
702
796
|
"""
|
|
@@ -734,7 +828,9 @@ class NPC:
|
|
|
734
828
|
self.jinxs_directory = None
|
|
735
829
|
self.npc_directory = None
|
|
736
830
|
|
|
737
|
-
self.team = team
|
|
831
|
+
self.team = team # Store the team reference (can be None)
|
|
832
|
+
self.jinxs_spec = jinxs or "*" # Store the jinx specification for later loading
|
|
833
|
+
|
|
738
834
|
if tools is not None:
|
|
739
835
|
tools_schema, tool_map = auto_tools(tools)
|
|
740
836
|
self.tools = tools_schema
|
|
@@ -755,6 +851,7 @@ class NPC:
|
|
|
755
851
|
if self.jinxs_directory:
|
|
756
852
|
dirs.append(self.jinxs_directory)
|
|
757
853
|
|
|
854
|
+
# This jinja_env is for the *second pass* (runtime variable resolution in Jinx.execute)
|
|
758
855
|
self.jinja_env = Environment(
|
|
759
856
|
loader=FileSystemLoader([
|
|
760
857
|
os.path.expanduser(d) for d in dirs
|
|
@@ -764,7 +861,6 @@ class NPC:
|
|
|
764
861
|
|
|
765
862
|
self.db_conn = db_conn
|
|
766
863
|
|
|
767
|
-
# these 4 get overwritten if the db conn
|
|
768
864
|
self.command_history = None
|
|
769
865
|
self.kg_data = None
|
|
770
866
|
self.tables = None
|
|
@@ -777,9 +873,24 @@ class NPC:
|
|
|
777
873
|
self.kg_data = self._load_npc_kg()
|
|
778
874
|
self.memory = self.get_memory_context()
|
|
779
875
|
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
876
|
+
self.jinxs_dict = {} # Initialize empty, will be populated by initialize_jinxs
|
|
877
|
+
# If jinxs are explicitly provided *to the NPC* during its standalone creation, load them.
|
|
878
|
+
# This is for NPCs created *outside* a team context initially.
|
|
879
|
+
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.")
|
|
783
894
|
|
|
784
895
|
self.shared_context = {
|
|
785
896
|
"dataframes": {},
|
|
@@ -794,6 +905,71 @@ class NPC:
|
|
|
794
905
|
if db_conn is not None:
|
|
795
906
|
init_db_tables()
|
|
796
907
|
|
|
908
|
+
def initialize_jinxs(self, team_raw_jinxs: Optional[List['Jinx']] = None):
|
|
909
|
+
"""
|
|
910
|
+
Loads and performs first-pass Jinja rendering for NPC-specific jinxs,
|
|
911
|
+
now that the NPC's team context is fully established.
|
|
912
|
+
"""
|
|
913
|
+
npc_jinxs_raw_list = []
|
|
914
|
+
|
|
915
|
+
# If jinxs_spec is "*", inherit all from team
|
|
916
|
+
if self.jinxs_spec == "*":
|
|
917
|
+
if self.team and hasattr(self.team, 'jinxs_dict') and self.team.jinxs_dict:
|
|
918
|
+
self.jinxs_dict.update(self.team.jinxs_dict)
|
|
919
|
+
else: # If specific jinxs are requested, try to get them from team
|
|
920
|
+
for jinx_name in self.jinxs_spec:
|
|
921
|
+
if self.team and jinx_name in self.team.jinxs_dict:
|
|
922
|
+
self.jinxs_dict[jinx_name] = self.team.jinxs_dict[jinx_name]
|
|
923
|
+
|
|
924
|
+
# Load NPC's own jinxs (if not already covered by team or if specific ones are requested)
|
|
925
|
+
if hasattr(self, 'npc_jinxs_directory') and self.npc_jinxs_directory and os.path.exists(self.npc_jinxs_directory):
|
|
926
|
+
for jinx_obj in load_jinxs_from_directory(self.npc_jinxs_directory):
|
|
927
|
+
if jinx_obj.jinx_name not in self.jinxs_dict: # Only add if not already added from team
|
|
928
|
+
npc_jinxs_raw_list.append(jinx_obj)
|
|
929
|
+
|
|
930
|
+
# If there are raw NPC jinxs to render or team_raw_jinxs available
|
|
931
|
+
if npc_jinxs_raw_list or team_raw_jinxs:
|
|
932
|
+
all_available_raw_jinxs = list(team_raw_jinxs or [])
|
|
933
|
+
all_available_raw_jinxs.extend(npc_jinxs_raw_list)
|
|
934
|
+
|
|
935
|
+
combined_raw_jinxs_dict = {j.jinx_name: j for j in all_available_raw_jinxs}
|
|
936
|
+
|
|
937
|
+
npc_first_pass_jinja_env = Environment(undefined=SilentUndefined)
|
|
938
|
+
|
|
939
|
+
jinx_macro_globals = {}
|
|
940
|
+
for raw_jinx in combined_raw_jinxs_dict.values():
|
|
941
|
+
def create_jinx_callable(jinx_obj_in_closure):
|
|
942
|
+
def callable_jinx(**kwargs):
|
|
943
|
+
temp_jinja_env = Environment(undefined=SilentUndefined)
|
|
944
|
+
rendered_target_steps = []
|
|
945
|
+
for target_step in jinx_obj_in_closure._raw_steps:
|
|
946
|
+
temp_rendered_step = {}
|
|
947
|
+
for k, v in target_step.items():
|
|
948
|
+
if isinstance(v, str):
|
|
949
|
+
try:
|
|
950
|
+
temp_rendered_step[k] = temp_jinja_env.from_string(v).render(**kwargs)
|
|
951
|
+
except Exception as e:
|
|
952
|
+
print(f"Warning: Error in Jinx macro '{jinx_obj_in_closure.jinx_name}' rendering step field '{k}' (NPC first pass): {e}")
|
|
953
|
+
temp_rendered_step[k] = v
|
|
954
|
+
else:
|
|
955
|
+
temp_rendered_step[k] = v
|
|
956
|
+
rendered_target_steps.append(temp_rendered_step)
|
|
957
|
+
return yaml.dump(rendered_target_steps, default_flow_style=False)
|
|
958
|
+
return callable_jinx
|
|
959
|
+
|
|
960
|
+
jinx_macro_globals[raw_jinx.jinx_name] = create_jinx_callable(raw_jinx)
|
|
961
|
+
|
|
962
|
+
npc_first_pass_jinja_env.globals.update(jinx_macro_globals)
|
|
963
|
+
|
|
964
|
+
for raw_npc_jinx in npc_jinxs_raw_list:
|
|
965
|
+
try:
|
|
966
|
+
raw_npc_jinx.render_first_pass(npc_first_pass_jinja_env, jinx_macro_globals)
|
|
967
|
+
self.jinxs_dict[raw_npc_jinx.jinx_name] = raw_npc_jinx
|
|
968
|
+
except Exception as e:
|
|
969
|
+
print(f"Error performing first-pass rendering for NPC Jinx '{raw_npc_jinx.jinx_name}': {e}")
|
|
970
|
+
|
|
971
|
+
print(f"NPC {self.name} loaded {len(self.jinxs_dict)} jinxs.")
|
|
972
|
+
|
|
797
973
|
def _load_npc_kg(self):
|
|
798
974
|
"""Load knowledge graph data for this NPC from database"""
|
|
799
975
|
if not self.command_history:
|
|
@@ -1093,45 +1269,6 @@ class NPC:
|
|
|
1093
1269
|
self.tables = None
|
|
1094
1270
|
self.db_type = None
|
|
1095
1271
|
|
|
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
1272
|
def get_llm_response(self,
|
|
1136
1273
|
request,
|
|
1137
1274
|
jinxs=None,
|
|
@@ -1225,7 +1362,7 @@ class NPC:
|
|
|
1225
1362
|
content = result.get('content', '')[:200] + ('...' if len(result.get('content', '')) > 200 else '')
|
|
1226
1363
|
formatted_results.append(f"[{timestamp}] {content}")
|
|
1227
1364
|
|
|
1228
|
-
return f"Found {len(results)} conversations matching '{query}':\n" + "\n".join(formatted_results)
|
|
1365
|
+
return f"Found {len(results)} conversations matching '{query}'s:\n" + "\n".join(formatted_results)
|
|
1229
1366
|
|
|
1230
1367
|
def search_my_memories(self, query: str, limit: int = 10) -> str:
|
|
1231
1368
|
"""Search through this NPC's knowledge graph memories for relevant facts and concepts"""
|
|
@@ -1298,113 +1435,6 @@ class NPC:
|
|
|
1298
1435
|
response = self.get_llm_response(thinking_prompt, tool_choice = False)
|
|
1299
1436
|
return response.get('response', 'Unable to process thinking request')
|
|
1300
1437
|
|
|
1301
|
-
def write_code(self, task_description: str, language: str = "python", show=True) -> str:
|
|
1302
|
-
"""Generate and execute code for a specific task, returning the result"""
|
|
1303
|
-
if language.lower() != "python":
|
|
1304
|
-
|
|
1305
|
-
code_prompt = f"""Write {language} code for the following task:
|
|
1306
|
-
{task_description}
|
|
1307
|
-
|
|
1308
|
-
Provide clean, working code with brief explanations for key parts:"""
|
|
1309
|
-
|
|
1310
|
-
response = self.get_llm_response(code_prompt, tool_choice=False )
|
|
1311
|
-
return response.get('response', 'Unable to generate code')
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
code_prompt = f"""Write Python code for the following task:
|
|
1315
|
-
{task_description}
|
|
1316
|
-
|
|
1317
|
-
Requirements:
|
|
1318
|
-
- Provide executable Python code
|
|
1319
|
-
- Store the final result in a variable called 'output'
|
|
1320
|
-
- Include any necessary imports
|
|
1321
|
-
- Handle errors gracefully
|
|
1322
|
-
- The code should be ready to execute without modification
|
|
1323
|
-
|
|
1324
|
-
Example format:
|
|
1325
|
-
```python
|
|
1326
|
-
import pandas as pd
|
|
1327
|
-
# Your code here
|
|
1328
|
-
result = some_calculation()
|
|
1329
|
-
output = f"Task completed successfully: {{result}}"
|
|
1330
|
-
"""
|
|
1331
|
-
response = self.get_llm_response(code_prompt, tool_choice= False)
|
|
1332
|
-
generated_code = response.get('response', '')
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
if '```python' in generated_code:
|
|
1336
|
-
code_lines = generated_code.split('\n')
|
|
1337
|
-
start_idx = None
|
|
1338
|
-
end_idx = None
|
|
1339
|
-
|
|
1340
|
-
for i, line in enumerate(code_lines):
|
|
1341
|
-
if '```python' in line:
|
|
1342
|
-
start_idx = i + 1
|
|
1343
|
-
elif '```' in line and start_idx is not None:
|
|
1344
|
-
end_idx = i
|
|
1345
|
-
break
|
|
1346
|
-
|
|
1347
|
-
if start_idx is not None:
|
|
1348
|
-
if end_idx is not None:
|
|
1349
|
-
generated_code = '\n'.join(code_lines[start_idx:end_idx])
|
|
1350
|
-
else:
|
|
1351
|
-
generated_code = '\n'.join(code_lines[start_idx:])
|
|
1352
|
-
|
|
1353
|
-
try:
|
|
1354
|
-
|
|
1355
|
-
exec_globals = {
|
|
1356
|
-
"__builtins__": __builtins__,
|
|
1357
|
-
"npc": self,
|
|
1358
|
-
"context": self.shared_context,
|
|
1359
|
-
"pd": pd,
|
|
1360
|
-
"plt": plt,
|
|
1361
|
-
"np": np,
|
|
1362
|
-
"os": os,
|
|
1363
|
-
"re": re,
|
|
1364
|
-
"json": json,
|
|
1365
|
-
"Path": pathlib.Path,
|
|
1366
|
-
"fnmatch": fnmatch,
|
|
1367
|
-
"pathlib": pathlib,
|
|
1368
|
-
"subprocess": subprocess,
|
|
1369
|
-
"datetime": datetime,
|
|
1370
|
-
"hashlib": hashlib,
|
|
1371
|
-
"sqlite3": sqlite3,
|
|
1372
|
-
"yaml": yaml,
|
|
1373
|
-
"random": random,
|
|
1374
|
-
"math": math,
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
exec_locals = {}
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
exec(generated_code, exec_globals, exec_locals)
|
|
1381
|
-
|
|
1382
|
-
if show:
|
|
1383
|
-
print('Executing code', generated_code)
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
if "output" in exec_locals:
|
|
1387
|
-
result = exec_locals["output"]
|
|
1388
|
-
|
|
1389
|
-
self.shared_context.update({k: v for k, v in exec_locals.items()
|
|
1390
|
-
if not k.startswith('_') and not callable(v)})
|
|
1391
|
-
return f"Code executed successfully. Result: {result}"
|
|
1392
|
-
else:
|
|
1393
|
-
|
|
1394
|
-
meaningful_vars = {k: v for k, v in exec_locals.items()
|
|
1395
|
-
if not k.startswith('_') and not callable(v)}
|
|
1396
|
-
|
|
1397
|
-
self.shared_context.update(meaningful_vars)
|
|
1398
|
-
|
|
1399
|
-
if meaningful_vars:
|
|
1400
|
-
last_var = list(meaningful_vars.items())[-1]
|
|
1401
|
-
return f"Code executed successfully. Last result: {last_var[0]} = {last_var[1]}"
|
|
1402
|
-
else:
|
|
1403
|
-
return "Code executed successfully (no explicit output generated)"
|
|
1404
|
-
|
|
1405
|
-
except Exception as e:
|
|
1406
|
-
error_msg = f"Error executing code: {str(e)}\n\nGenerated code was:\n{generated_code}"
|
|
1407
|
-
return error_msg
|
|
1408
1438
|
|
|
1409
1439
|
|
|
1410
1440
|
|
|
@@ -1621,23 +1651,29 @@ class NPC:
|
|
|
1621
1651
|
"compressed_state": self.compress_planning_state(planning_state),
|
|
1622
1652
|
"summary": f"Completed {len(planning_state['successes'])} tasks for goal: {user_goal}"
|
|
1623
1653
|
}
|
|
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
1654
|
|
|
1655
|
+
def execute_jinx(
|
|
1656
|
+
self,
|
|
1657
|
+
jinx_name,
|
|
1658
|
+
inputs,
|
|
1659
|
+
conversation_id=None,
|
|
1660
|
+
message_id=None,
|
|
1661
|
+
team_name=None,
|
|
1662
|
+
extra_globals=None
|
|
1663
|
+
):
|
|
1628
1664
|
if jinx_name in self.jinxs_dict:
|
|
1629
1665
|
jinx = self.jinxs_dict[jinx_name]
|
|
1630
|
-
elif jinx_name in self.jinxs_dict:
|
|
1631
|
-
jinx = self.jinxs_dict[jinx_name]
|
|
1632
1666
|
else:
|
|
1633
1667
|
return {"error": f"jinx '{jinx_name}' not found"}
|
|
1634
1668
|
|
|
1635
1669
|
result = jinx.execute(
|
|
1636
1670
|
input_values=inputs,
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1671
|
+
npc=self,
|
|
1672
|
+
# messages=messages, # messages should be passed from the calling context if available
|
|
1673
|
+
extra_globals=extra_globals,
|
|
1674
|
+
jinja_env=self.jinja_env # Pass the NPC's second-pass Jinja env
|
|
1640
1675
|
)
|
|
1676
|
+
|
|
1641
1677
|
if self.db_conn is not None:
|
|
1642
1678
|
self.db_conn.add_jinx_call(
|
|
1643
1679
|
triggering_message_id=message_id,
|
|
@@ -1652,7 +1688,6 @@ class NPC:
|
|
|
1652
1688
|
team_name=team_name,
|
|
1653
1689
|
)
|
|
1654
1690
|
return result
|
|
1655
|
-
|
|
1656
1691
|
def check_llm_command(self,
|
|
1657
1692
|
command,
|
|
1658
1693
|
messages=None,
|
|
@@ -1722,8 +1757,8 @@ class NPC:
|
|
|
1722
1757
|
def to_dict(self):
|
|
1723
1758
|
"""Convert NPC to dictionary representation"""
|
|
1724
1759
|
jinx_rep = []
|
|
1725
|
-
if self.
|
|
1726
|
-
jinx_rep = [ jinx.to_dict()
|
|
1760
|
+
if self.jinxs_dict: # Use jinxs_dict which stores the rendered Jinx objects
|
|
1761
|
+
jinx_rep = [ jinx.to_dict() for jinx in self.jinxs_dict.values()]
|
|
1727
1762
|
return {
|
|
1728
1763
|
"name": self.name,
|
|
1729
1764
|
"primary_directive": self.primary_directive,
|
|
@@ -1731,7 +1766,7 @@ class NPC:
|
|
|
1731
1766
|
"provider": self.provider,
|
|
1732
1767
|
"api_url": self.api_url,
|
|
1733
1768
|
"api_key": self.api_key,
|
|
1734
|
-
"jinxs":
|
|
1769
|
+
"jinxs": self.jinxs_spec, # Save the original spec, not the rendered objects
|
|
1735
1770
|
"use_global_jinxs": self.use_global_jinxs
|
|
1736
1771
|
}
|
|
1737
1772
|
|
|
@@ -1748,10 +1783,10 @@ class NPC:
|
|
|
1748
1783
|
def __str__(self):
|
|
1749
1784
|
"""String representation of NPC"""
|
|
1750
1785
|
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.
|
|
1786
|
+
if self.jinxs_dict:
|
|
1752
1787
|
str_rep += "Jinxs:\n"
|
|
1753
|
-
for
|
|
1754
|
-
str_rep += f" - {
|
|
1788
|
+
for jinx_name in self.jinxs_dict.keys():
|
|
1789
|
+
str_rep += f" - {jinx_name}\n"
|
|
1755
1790
|
else:
|
|
1756
1791
|
str_rep += "No jinxs available.\n"
|
|
1757
1792
|
return str_rep
|
|
@@ -1769,13 +1804,11 @@ class NPC:
|
|
|
1769
1804
|
|
|
1770
1805
|
input_values = extract_jinx_inputs(args, jinx)
|
|
1771
1806
|
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
1807
|
jinx_output = jinx.execute(
|
|
1776
1808
|
input_values,
|
|
1777
|
-
jinx.jinx_name,
|
|
1778
1809
|
npc=self,
|
|
1810
|
+
messages=messages, # Pass messages to Jinx.execute
|
|
1811
|
+
jinja_env=self.jinja_env # Pass the NPC's second-pass Jinja env
|
|
1779
1812
|
)
|
|
1780
1813
|
|
|
1781
1814
|
return {"messages": messages, "output": jinx_output}
|
|
@@ -1937,12 +1970,14 @@ class NPC:
|
|
|
1937
1970
|
class Team:
|
|
1938
1971
|
def __init__(self,
|
|
1939
1972
|
team_path=None,
|
|
1940
|
-
npcs=None,
|
|
1941
|
-
forenpc=None,
|
|
1942
|
-
jinxs=None,
|
|
1973
|
+
npcs: Optional[List['NPC']] = None, # Explicitly type hint as list of NPC
|
|
1974
|
+
forenpc: Optional[Union[str, 'NPC']] = None, # Can be name (str) or NPC object
|
|
1975
|
+
jinxs: Optional[List[Union['Jinx', Dict[str, Any]]]] = None, # List of raw Jinx objects or dicts
|
|
1943
1976
|
db_conn=None,
|
|
1944
1977
|
model = None,
|
|
1945
|
-
provider = None
|
|
1978
|
+
provider = None,
|
|
1979
|
+
api_url = None,
|
|
1980
|
+
api_key = None):
|
|
1946
1981
|
"""
|
|
1947
1982
|
Initialize an NPC team from directory or list of NPCs
|
|
1948
1983
|
|
|
@@ -1953,45 +1988,249 @@ class Team:
|
|
|
1953
1988
|
"""
|
|
1954
1989
|
self.model = model
|
|
1955
1990
|
self.provider = provider
|
|
1991
|
+
self.api_url = api_url
|
|
1992
|
+
self.api_key = api_key
|
|
1956
1993
|
|
|
1957
|
-
self.npcs = {}
|
|
1958
|
-
self.sub_teams = {}
|
|
1959
|
-
self.jinxs_dict
|
|
1994
|
+
self.npcs: Dict[str, 'NPC'] = {} # Store NPC objects by name
|
|
1995
|
+
self.sub_teams: Dict[str, 'Team'] = {}
|
|
1996
|
+
self.jinxs_dict: Dict[str, 'Jinx'] = {} # This will store first-pass rendered Jinx objects
|
|
1997
|
+
self._raw_jinxs_list: List['Jinx'] = [] # Temporary storage for raw Team-level Jinx objects
|
|
1998
|
+
|
|
1999
|
+
self.jinja_env_for_first_pass = Environment(undefined=SilentUndefined) # Env for macro expansion
|
|
2000
|
+
|
|
1960
2001
|
self.db_conn = db_conn
|
|
1961
2002
|
self.team_path = os.path.expanduser(team_path) if team_path else None
|
|
1962
2003
|
self.databases = []
|
|
1963
2004
|
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
2005
|
|
|
2006
|
+
self.forenpc: Optional['NPC'] = None # Will be set to an NPC object by end of __init__
|
|
2007
|
+
self.forenpc_name: Optional[str] = None # Temporary storage for name from context (if loaded from .ctx)
|
|
2008
|
+
|
|
1969
2009
|
if team_path:
|
|
1970
2010
|
self.name = os.path.basename(os.path.abspath(team_path))
|
|
1971
|
-
|
|
2011
|
+
self._load_from_directory_and_initialize_forenpc()
|
|
2012
|
+
elif npcs:
|
|
1972
2013
|
self.name = "custom_team"
|
|
2014
|
+
# Add provided NPCs and set their team attribute
|
|
2015
|
+
for npc_obj in npcs:
|
|
2016
|
+
self.npcs[npc_obj.name] = npc_obj
|
|
2017
|
+
npc_obj.team = self # Crucial: set the team for pre-existing NPCs
|
|
2018
|
+
|
|
2019
|
+
if jinxs: # Load raw team-level jinxs if provided
|
|
2020
|
+
for jinx_item in jinxs:
|
|
2021
|
+
if isinstance(jinx_item, Jinx):
|
|
2022
|
+
self._raw_jinxs_list.append(jinx_item)
|
|
2023
|
+
elif isinstance(jinx_item, dict):
|
|
2024
|
+
self._raw_jinxs_list.append(Jinx(jinx_data=jinx_item))
|
|
2025
|
+
# Assuming string jinxs are paths or names to be loaded later if needed.
|
|
2026
|
+
|
|
2027
|
+
self._determine_forenpc_from_provided_npcs(npcs, forenpc)
|
|
2028
|
+
|
|
2029
|
+
else: # No team_path and no npcs list, create a default forenpc
|
|
2030
|
+
self.name = "custom_team"
|
|
2031
|
+
self._create_default_forenpc()
|
|
2032
|
+
|
|
1973
2033
|
self.context = ''
|
|
1974
2034
|
self.shared_context = {
|
|
1975
2035
|
"intermediate_results": {},
|
|
1976
2036
|
"dataframes": {},
|
|
1977
2037
|
"memories": {},
|
|
1978
2038
|
"execution_history": [],
|
|
1979
|
-
"npc_messages": {},
|
|
1980
2039
|
"context":''
|
|
1981
2040
|
}
|
|
1982
|
-
|
|
2041
|
+
|
|
2042
|
+
# Load team context into shared_context after forenpc is determined
|
|
2043
|
+
# This is for teams loaded from directory. For custom/default teams, context is set below.
|
|
1983
2044
|
if team_path:
|
|
1984
|
-
self.
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
self.
|
|
2045
|
+
self._load_team_context_into_shared_context()
|
|
2046
|
+
elif self.forenpc: # For custom teams or default, set basic context if not already set
|
|
2047
|
+
if not self.context: # Only set if context is still empty
|
|
2048
|
+
self.context = f"Team '{self.name}' with forenpc '{self.forenpc.name}'"
|
|
2049
|
+
self.shared_context['context'] = self.context
|
|
2050
|
+
|
|
2051
|
+
# Perform first-pass rendering for team-level jinxs
|
|
2052
|
+
self._perform_first_pass_jinx_rendering()
|
|
1989
2053
|
|
|
1990
|
-
|
|
2054
|
+
# Now, initialize jinxs for all NPCs, as team-level jinxs are ready
|
|
2055
|
+
for npc_obj in self.npcs.values():
|
|
2056
|
+
# Pass the team's raw jinxs to the NPC for its own first-pass rendering
|
|
2057
|
+
npc_obj.initialize_jinxs(team_raw_jinxs=self._raw_jinxs_list)
|
|
1991
2058
|
|
|
1992
2059
|
if db_conn is not None:
|
|
1993
2060
|
init_db_tables()
|
|
1994
2061
|
|
|
2062
|
+
def _load_from_directory_and_initialize_forenpc(self):
|
|
2063
|
+
"""
|
|
2064
|
+
Consolidated method to load NPCs, team context, and resolve the forenpc.
|
|
2065
|
+
Ensures self.npcs is populated and self.forenpc is an NPC object.
|
|
2066
|
+
"""
|
|
2067
|
+
if not os.path.exists(self.team_path):
|
|
2068
|
+
raise ValueError(f"Team directory not found: {self.team_path}")
|
|
2069
|
+
|
|
2070
|
+
# 1. Load all NPCs first (without initializing their jinxs yet)
|
|
2071
|
+
for filename in os.listdir(self.team_path):
|
|
2072
|
+
if filename.endswith(".npc"):
|
|
2073
|
+
npc_path = os.path.join(self.team_path, filename)
|
|
2074
|
+
# Pass 'self' to NPC constructor for team reference
|
|
2075
|
+
# Do NOT pass jinxs=... here, as it will be initialized later
|
|
2076
|
+
npc = NPC(npc_path, db_conn=self.db_conn, team=self)
|
|
2077
|
+
self.npcs[npc.name] = npc
|
|
2078
|
+
|
|
2079
|
+
# 2. Load team context and determine forenpc name (string)
|
|
2080
|
+
self._load_team_context_file() # This populates self.model, self.provider, self.forenpc_name etc.
|
|
2081
|
+
|
|
2082
|
+
# 3. Resolve and set self.forenpc (NPC object)
|
|
2083
|
+
if self.forenpc_name and self.forenpc_name in self.npcs:
|
|
2084
|
+
self.forenpc = self.npcs[self.forenpc_name]
|
|
2085
|
+
elif self.npcs: # Fallback to first NPC if name not found or not specified
|
|
2086
|
+
self.forenpc = list(self.npcs.values())[0]
|
|
2087
|
+
self.forenpc_name = self.forenpc.name # Update forenpc_name for consistency
|
|
2088
|
+
else: # No NPCs loaded, create a default forenpc
|
|
2089
|
+
self._create_default_forenpc()
|
|
2090
|
+
|
|
2091
|
+
# 4. Load raw Jinxs from team directory
|
|
2092
|
+
jinxs_dir = os.path.join(self.team_path, "jinxs")
|
|
2093
|
+
if os.path.exists(jinxs_dir):
|
|
2094
|
+
for jinx_obj in load_jinxs_from_directory(jinxs_dir):
|
|
2095
|
+
self._raw_jinxs_list.append(jinx_obj)
|
|
2096
|
+
|
|
2097
|
+
# 5. Load sub-teams
|
|
2098
|
+
self._load_sub_teams()
|
|
2099
|
+
|
|
2100
|
+
def _load_team_context_file(self) -> Dict[str, Any]:
|
|
2101
|
+
"""Loads team context from .ctx file and updates team attributes."""
|
|
2102
|
+
ctx_data = {}
|
|
2103
|
+
for fname in os.listdir(self.team_path):
|
|
2104
|
+
if fname.endswith('.ctx'):
|
|
2105
|
+
ctx_data = load_yaml_file(os.path.join(self.team_path, fname))
|
|
2106
|
+
if ctx_data is not None:
|
|
2107
|
+
self.model = ctx_data.get('model', self.model)
|
|
2108
|
+
self.provider = ctx_data.get('provider', self.provider)
|
|
2109
|
+
self.api_url = ctx_data.get('api_url', self.api_url)
|
|
2110
|
+
self.env = ctx_data.get('env', self.env if hasattr(self, 'env') else None)
|
|
2111
|
+
self.mcp_servers = ctx_data.get('mcp_servers', [])
|
|
2112
|
+
self.databases = ctx_data.get('databases', [])
|
|
2113
|
+
self.forenpc_name = ctx_data.get('forenpc', self.forenpc_name) # Set forenpc_name (string)
|
|
2114
|
+
return ctx_data
|
|
2115
|
+
return {}
|
|
2116
|
+
|
|
2117
|
+
def _load_team_context_into_shared_context(self):
|
|
2118
|
+
"""Loads team context into shared_context after forenpc is determined."""
|
|
2119
|
+
ctx_data = {}
|
|
2120
|
+
for fname in os.listdir(self.team_path):
|
|
2121
|
+
if fname.endswith('.ctx'):
|
|
2122
|
+
ctx_data = load_yaml_file(os.path.join(self.team_path, fname))
|
|
2123
|
+
if ctx_data is not None:
|
|
2124
|
+
self.context = ctx_data.get('context', '')
|
|
2125
|
+
self.shared_context['context'] = self.context
|
|
2126
|
+
if 'file_patterns' in ctx_data:
|
|
2127
|
+
file_cache = self._parse_file_patterns(ctx_data['file_patterns'])
|
|
2128
|
+
self.shared_context['files'] = file_cache
|
|
2129
|
+
if 'preferences' in ctx_data:
|
|
2130
|
+
self.preferences = ctx_data['preferences']
|
|
2131
|
+
else:
|
|
2132
|
+
self.preferences = []
|
|
2133
|
+
|
|
2134
|
+
for key, item in ctx_data.items():
|
|
2135
|
+
if key not in ['name', 'mcp_servers', 'databases', 'context', 'file_patterns', 'forenpc', 'model', 'provider', 'api_url', 'env', 'preferences']:
|
|
2136
|
+
self.shared_context[key] = item
|
|
2137
|
+
return # Only load the first .ctx file found
|
|
2138
|
+
|
|
2139
|
+
def _determine_forenpc_from_provided_npcs(self, npcs_list: List['NPC'], forenpc_arg: Optional[Union[str, 'NPC']]):
|
|
2140
|
+
"""Determines self.forenpc when NPCs are provided directly to Team.__init__."""
|
|
2141
|
+
if forenpc_arg:
|
|
2142
|
+
if isinstance(forenpc_arg, NPC):
|
|
2143
|
+
self.forenpc = forenpc_arg
|
|
2144
|
+
self.forenpc_name = forenpc_arg.name
|
|
2145
|
+
elif isinstance(forenpc_arg, str) and forenpc_arg in self.npcs:
|
|
2146
|
+
self.forenpc = self.npcs[forenpc_arg]
|
|
2147
|
+
self.forenpc_name = forenpc_arg
|
|
2148
|
+
else:
|
|
2149
|
+
print(f"Warning: Specified forenpc '{forenpc_arg}' not found among provided NPCs. Falling back to first NPC.")
|
|
2150
|
+
if npcs_list:
|
|
2151
|
+
self.forenpc = npcs_list[0]
|
|
2152
|
+
self.forenpc_name = npcs_list[0].name
|
|
2153
|
+
else:
|
|
2154
|
+
self._create_default_forenpc()
|
|
2155
|
+
elif npcs_list: # Default to first NPC if no forenpc_arg
|
|
2156
|
+
self.forenpc = npcs_list[0]
|
|
2157
|
+
self.forenpc_name = npcs_list[0].name
|
|
2158
|
+
else: # No NPCs provided, create a default forenpc
|
|
2159
|
+
self._create_default_forenpc()
|
|
2160
|
+
|
|
2161
|
+
def _create_default_forenpc(self):
|
|
2162
|
+
"""Creates a default forenpc if none can be determined."""
|
|
2163
|
+
forenpc_model = self.model or 'llama3.2'
|
|
2164
|
+
forenpc_provider = self.provider or 'ollama'
|
|
2165
|
+
forenpc_api_key = self.api_key
|
|
2166
|
+
forenpc_api_url = self.api_url
|
|
2167
|
+
|
|
2168
|
+
default_forenpc = NPC(name='forenpc',
|
|
2169
|
+
primary_directive="""You are the forenpc of the team, coordinating activities
|
|
2170
|
+
between NPCs on the team, verifying that results from
|
|
2171
|
+
NPCs are high quality and can help to adequately answer
|
|
2172
|
+
user requests.""",
|
|
2173
|
+
model=forenpc_model,
|
|
2174
|
+
provider=forenpc_provider,
|
|
2175
|
+
api_key=forenpc_api_key,
|
|
2176
|
+
api_url=forenpc_api_url,
|
|
2177
|
+
team=self # Pass the team to the forenpc
|
|
2178
|
+
)
|
|
2179
|
+
self.forenpc = default_forenpc
|
|
2180
|
+
self.forenpc_name = default_forenpc.name
|
|
2181
|
+
self.npcs[default_forenpc.name] = default_forenpc # Add to team's NPC list
|
|
2182
|
+
|
|
2183
|
+
def _perform_first_pass_jinx_rendering(self):
|
|
2184
|
+
"""
|
|
2185
|
+
Performs the first-pass Jinja rendering on all loaded raw Jinxs.
|
|
2186
|
+
This expands nested Jinx calls but preserves runtime variables.
|
|
2187
|
+
"""
|
|
2188
|
+
# Create Jinja globals for calling other Jinxs as macros
|
|
2189
|
+
jinx_macro_globals = {}
|
|
2190
|
+
for raw_jinx in self._raw_jinxs_list:
|
|
2191
|
+
def create_jinx_callable(jinx_obj_in_closure):
|
|
2192
|
+
def callable_jinx(**kwargs):
|
|
2193
|
+
# This callable will be invoked by the Jinja renderer during the first pass.
|
|
2194
|
+
# It needs to render the target Jinx's *raw* steps with the provided kwargs.
|
|
2195
|
+
temp_jinja_env = Environment(undefined=SilentUndefined)
|
|
2196
|
+
|
|
2197
|
+
rendered_target_steps = []
|
|
2198
|
+
for target_step in jinx_obj_in_closure._raw_steps:
|
|
2199
|
+
temp_rendered_step = {}
|
|
2200
|
+
for k, v in target_step.items():
|
|
2201
|
+
if isinstance(v, str):
|
|
2202
|
+
try:
|
|
2203
|
+
# Render the string, using kwargs as context.
|
|
2204
|
+
# SilentUndefined will ensure {{ var }} that are not in kwargs remain as is.
|
|
2205
|
+
temp_rendered_step[k] = temp_jinja_env.from_string(v).render(**kwargs)
|
|
2206
|
+
except Exception as e:
|
|
2207
|
+
print(f"Warning: Error in Jinx macro '{jinx_obj_in_closure.jinx_name}' rendering step field '{k}' (Team first pass): {e}")
|
|
2208
|
+
temp_rendered_step[k] = v
|
|
2209
|
+
else:
|
|
2210
|
+
temp_rendered_step[k] = v
|
|
2211
|
+
rendered_target_steps.append(temp_rendered_step)
|
|
2212
|
+
|
|
2213
|
+
# Return the YAML string representation of the rendered steps
|
|
2214
|
+
return yaml.dump(rendered_target_steps, default_flow_style=False)
|
|
2215
|
+
return callable_jinx
|
|
2216
|
+
|
|
2217
|
+
jinx_macro_globals[raw_jinx.jinx_name] = create_jinx_callable(raw_jinx)
|
|
2218
|
+
|
|
2219
|
+
self.jinja_env_for_first_pass.globals['jinxs'] = jinx_macro_globals # Make 'jinxs.jinx_name' callable
|
|
2220
|
+
self.jinja_env_for_first_pass.globals.update(jinx_macro_globals) # Also make 'jinx_name' callable directly
|
|
2221
|
+
|
|
2222
|
+
# Now, iterate through the raw Jinxs and perform the first-pass rendering
|
|
2223
|
+
for raw_jinx in self._raw_jinxs_list:
|
|
2224
|
+
try:
|
|
2225
|
+
# Pass the jinx_macro_globals to render_first_pass so it can resolve declarative calls
|
|
2226
|
+
raw_jinx.render_first_pass(self.jinja_env_for_first_pass, jinx_macro_globals)
|
|
2227
|
+
self.jinxs_dict[raw_jinx.jinx_name] = raw_jinx # Store the first-pass rendered Jinx
|
|
2228
|
+
except Exception as e:
|
|
2229
|
+
print(f"Error performing first-pass rendering for Jinx '{raw_jinx.jinx_name}': {e}")
|
|
2230
|
+
|
|
2231
|
+
self._raw_jinxs_list = [] # Clear temporary storage
|
|
2232
|
+
|
|
2233
|
+
|
|
1995
2234
|
def update_context(self, messages: list):
|
|
1996
2235
|
"""Update team context based on recent conversation patterns"""
|
|
1997
2236
|
if len(messages) < 10:
|
|
@@ -2035,77 +2274,6 @@ class Team:
|
|
|
2035
2274
|
with open(team_ctx_path, 'w') as f:
|
|
2036
2275
|
yaml.dump(ctx_data, f)
|
|
2037
2276
|
|
|
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
2277
|
def _load_sub_teams(self):
|
|
2110
2278
|
"""Load sub-teams from subdirectories"""
|
|
2111
2279
|
for item in os.listdir(self.team_path):
|
|
@@ -2119,47 +2287,14 @@ class Team:
|
|
|
2119
2287
|
sub_team = Team(team_path=item_path, db_conn=self.db_conn)
|
|
2120
2288
|
self.sub_teams[item] = sub_team
|
|
2121
2289
|
|
|
2122
|
-
def get_forenpc(self):
|
|
2290
|
+
def get_forenpc(self) -> Optional['NPC']:
|
|
2123
2291
|
"""
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2292
|
+
Returns the forenpc (coordinator) for this team.
|
|
2293
|
+
This method is now primarily for external access, as self.forenpc is set in __init__.
|
|
2127
2294
|
"""
|
|
2128
|
-
|
|
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
|
|
2295
|
+
return self.forenpc
|
|
2161
2296
|
|
|
2162
|
-
def get_npc(self, npc_ref):
|
|
2297
|
+
def get_npc(self, npc_ref: Union[str, 'NPC']) -> Optional['NPC']:
|
|
2163
2298
|
"""Get NPC by name or reference with hierarchical lookup capability"""
|
|
2164
2299
|
if isinstance(npc_ref, NPC):
|
|
2165
2300
|
return npc_ref
|
|
@@ -2181,7 +2316,7 @@ class Team:
|
|
|
2181
2316
|
|
|
2182
2317
|
def orchestrate(self, request):
|
|
2183
2318
|
"""Orchestrate a request through the team"""
|
|
2184
|
-
forenpc = self.get_forenpc()
|
|
2319
|
+
forenpc = self.get_forenpc() # Now guaranteed to be an NPC object
|
|
2185
2320
|
if not forenpc:
|
|
2186
2321
|
return {"error": "No forenpc available to coordinate the team"}
|
|
2187
2322
|
|
|
@@ -2326,7 +2461,7 @@ class Team:
|
|
|
2326
2461
|
"name": self.name,
|
|
2327
2462
|
"npcs": {name: npc.to_dict() for name, npc in self.npcs.items()},
|
|
2328
2463
|
"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.
|
|
2464
|
+
"jinxs": {name: jinx.to_dict() for name, jinx in self.jinxs_dict.items()}, # Use jinxs_dict
|
|
2330
2465
|
"context": getattr(self, 'context', {})
|
|
2331
2466
|
}
|
|
2332
2467
|
|
|
@@ -2350,7 +2485,7 @@ class Team:
|
|
|
2350
2485
|
jinxs_dir = os.path.join(directory, "jinxs")
|
|
2351
2486
|
ensure_dirs_exist(jinxs_dir)
|
|
2352
2487
|
|
|
2353
|
-
for jinx in self.
|
|
2488
|
+
for jinx in self.jinxs_dict.values(): # Use jinxs_dict
|
|
2354
2489
|
jinx.save(jinxs_dir)
|
|
2355
2490
|
|
|
2356
2491
|
for team_name, team in self.sub_teams.items():
|