npcpy 1.2.36__py3-none-any.whl → 1.2.37__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/__init__.py +10 -2
- npcpy/gen/image_gen.py +5 -2
- npcpy/gen/response.py +262 -64
- npcpy/llm_funcs.py +478 -832
- npcpy/ml_funcs.py +746 -0
- npcpy/npc_array.py +1294 -0
- npcpy/npc_compiler.py +320 -257
- npcpy/npc_sysenv.py +17 -2
- npcpy/serve.py +162 -14
- npcpy/sql/npcsql.py +96 -59
- {npcpy-1.2.36.dist-info → npcpy-1.2.37.dist-info}/METADATA +173 -1
- {npcpy-1.2.36.dist-info → npcpy-1.2.37.dist-info}/RECORD +15 -13
- {npcpy-1.2.36.dist-info → npcpy-1.2.37.dist-info}/WHEEL +0 -0
- {npcpy-1.2.36.dist-info → npcpy-1.2.37.dist-info}/licenses/LICENSE +0 -0
- {npcpy-1.2.36.dist-info → npcpy-1.2.37.dist-info}/top_level.txt +0 -0
npcpy/npc_compiler.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import shutil
|
|
2
3
|
from pyexpat.errors import messages
|
|
3
4
|
import yaml
|
|
4
5
|
import json
|
|
@@ -18,7 +19,6 @@ from typing import Any, Dict, List, Optional, Union, Callable, Tuple
|
|
|
18
19
|
from jinja2 import Environment, FileSystemLoader, Template, Undefined, DictLoader
|
|
19
20
|
from sqlalchemy import create_engine, text
|
|
20
21
|
import npcpy as npy
|
|
21
|
-
from npcpy.llm_funcs import DEFAULT_ACTION_SPACE
|
|
22
22
|
from npcpy.tools import auto_tools
|
|
23
23
|
import math
|
|
24
24
|
import random
|
|
@@ -183,6 +183,7 @@ def initialize_npc_project(
|
|
|
183
183
|
"""Initialize an NPC project"""
|
|
184
184
|
if directory is None:
|
|
185
185
|
directory = os.getcwd()
|
|
186
|
+
directory = os.path.expanduser(os.fspath(directory))
|
|
186
187
|
|
|
187
188
|
npc_team_dir = os.path.join(directory, "npc_team")
|
|
188
189
|
os.makedirs(npc_team_dir, exist_ok=True)
|
|
@@ -191,7 +192,8 @@ def initialize_npc_project(
|
|
|
191
192
|
"assembly_lines",
|
|
192
193
|
"sql_models",
|
|
193
194
|
"jobs",
|
|
194
|
-
"triggers"
|
|
195
|
+
"triggers",
|
|
196
|
+
"tools"]:
|
|
195
197
|
os.makedirs(os.path.join(npc_team_dir, subdir), exist_ok=True)
|
|
196
198
|
|
|
197
199
|
forenpc_path = os.path.join(npc_team_dir, "forenpc.npc")
|
|
@@ -206,20 +208,166 @@ def initialize_npc_project(
|
|
|
206
208
|
}
|
|
207
209
|
with open(forenpc_path, "w") as f:
|
|
208
210
|
yaml.dump(default_npc, f)
|
|
209
|
-
|
|
210
|
-
if
|
|
211
|
+
parsed_templates: List[str] = []
|
|
212
|
+
if templates:
|
|
213
|
+
if isinstance(templates, str):
|
|
214
|
+
parsed_templates = [
|
|
215
|
+
t.strip() for t in re.split(r"[,\s]+", templates) if t.strip()
|
|
216
|
+
]
|
|
217
|
+
elif isinstance(templates, (list, tuple, set)):
|
|
218
|
+
parsed_templates = [str(t).strip() for t in templates if str(t).strip()]
|
|
219
|
+
else:
|
|
220
|
+
parsed_templates = [str(templates).strip()]
|
|
221
|
+
|
|
222
|
+
ctx_destination: Optional[str] = None
|
|
223
|
+
preexisting_ctx = [
|
|
224
|
+
os.path.join(npc_team_dir, f)
|
|
225
|
+
for f in os.listdir(npc_team_dir)
|
|
226
|
+
if f.endswith(".ctx")
|
|
227
|
+
]
|
|
228
|
+
if preexisting_ctx:
|
|
229
|
+
ctx_destination = preexisting_ctx[0]
|
|
230
|
+
if len(preexisting_ctx) > 1:
|
|
231
|
+
print(
|
|
232
|
+
"Warning: Multiple .ctx files already present; using first and ignoring the rest."
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
def _resolve_template_path(template_name: str) -> Optional[str]:
|
|
236
|
+
expanded = os.path.expanduser(template_name)
|
|
237
|
+
if os.path.exists(expanded):
|
|
238
|
+
return expanded
|
|
239
|
+
|
|
240
|
+
embedded_templates = {
|
|
241
|
+
"slean": """name: slean
|
|
242
|
+
primary_directive: You are slean, the marketing innovator AI. Your responsibility is to create marketing campaigns and manage them effectively, while also thinking creatively to solve marketing challenges. Guide the strategy that drives customer engagement and brand awareness.
|
|
243
|
+
""",
|
|
244
|
+
"turnic": """name: turnic
|
|
245
|
+
primary_directive: Assist with sales challenges and questions. Opt for straightforward solutions that help sales professionals achieve quick results.
|
|
246
|
+
""",
|
|
247
|
+
"budgeto": """name: budgeto
|
|
248
|
+
primary_directive: You manage marketing budgets, ensuring resources are allocated efficiently and spend is optimized.
|
|
249
|
+
""",
|
|
250
|
+
"relatio": """name: relatio
|
|
251
|
+
primary_directive: You manage customer relationships and ensure satisfaction throughout the sales process. Focus on nurturing clients and maintaining long-term connections.
|
|
252
|
+
""",
|
|
253
|
+
"funnel": """name: funnel
|
|
254
|
+
primary_directive: You oversee the sales pipeline, track progress, and optimize conversion rates to move leads efficiently.
|
|
255
|
+
""",
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
base_dirs = [
|
|
259
|
+
os.path.expanduser("~/.npcsh/npc_team/templates"),
|
|
260
|
+
os.path.expanduser("~/.npcpy/npc_team/templates"),
|
|
261
|
+
os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "tests", "template_tests", "npc_team")),
|
|
262
|
+
os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "examples", "npc_team")),
|
|
263
|
+
os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "example_npc_project", "npc_team")),
|
|
264
|
+
]
|
|
265
|
+
base_dirs = [d for d in base_dirs if os.path.isdir(d)]
|
|
266
|
+
|
|
267
|
+
for base in base_dirs:
|
|
268
|
+
direct = os.path.join(base, template_name)
|
|
269
|
+
if os.path.exists(direct):
|
|
270
|
+
return direct
|
|
271
|
+
if not direct.endswith(".npc") and os.path.exists(direct + ".npc"):
|
|
272
|
+
return direct + ".npc"
|
|
273
|
+
for root, _, files in os.walk(base):
|
|
274
|
+
for fname in files:
|
|
275
|
+
stem, ext = os.path.splitext(fname)
|
|
276
|
+
if ext == ".npc" and stem == template_name:
|
|
277
|
+
return os.path.join(root, fname)
|
|
278
|
+
|
|
279
|
+
# If no on-disk template found, fall back to embedded definitions
|
|
280
|
+
if template_name in embedded_templates:
|
|
281
|
+
embedded_dir = os.path.join(npc_team_dir, "_embedded_templates", template_name)
|
|
282
|
+
os.makedirs(embedded_dir, exist_ok=True)
|
|
283
|
+
npc_file = os.path.join(embedded_dir, f"{template_name}.npc")
|
|
284
|
+
if not os.path.exists(npc_file):
|
|
285
|
+
with open(npc_file, "w") as f:
|
|
286
|
+
f.write(embedded_templates[template_name])
|
|
287
|
+
return embedded_dir
|
|
288
|
+
return None
|
|
289
|
+
|
|
290
|
+
def _copy_template(src_path: str) -> List[str]:
|
|
291
|
+
nonlocal ctx_destination
|
|
292
|
+
copied: List[str] = []
|
|
293
|
+
src_path = os.path.expanduser(src_path)
|
|
294
|
+
|
|
295
|
+
allowed_exts = {".npc", ".tool", ".pipe", ".sql", ".job", ".ctx", ".yaml", ".yml"}
|
|
296
|
+
|
|
297
|
+
if os.path.isfile(src_path):
|
|
298
|
+
if os.path.splitext(src_path)[1] in allowed_exts:
|
|
299
|
+
if os.path.splitext(src_path)[1] == ".ctx":
|
|
300
|
+
if ctx_destination:
|
|
301
|
+
print(
|
|
302
|
+
f"Warning: Skipping extra context file '{src_path}' because one already exists."
|
|
303
|
+
)
|
|
304
|
+
return copied
|
|
305
|
+
dest_path = os.path.join(npc_team_dir, os.path.basename(src_path))
|
|
306
|
+
ctx_destination = dest_path
|
|
307
|
+
else:
|
|
308
|
+
dest_path = os.path.join(npc_team_dir, os.path.basename(src_path))
|
|
309
|
+
if not os.path.exists(dest_path):
|
|
310
|
+
shutil.copy2(src_path, dest_path)
|
|
311
|
+
copied.append(dest_path)
|
|
312
|
+
return copied
|
|
313
|
+
|
|
314
|
+
for root, _, files in os.walk(src_path):
|
|
315
|
+
rel_dir = os.path.relpath(root, src_path)
|
|
316
|
+
dest_dir = npc_team_dir if rel_dir == "." else os.path.join(npc_team_dir, rel_dir)
|
|
317
|
+
os.makedirs(dest_dir, exist_ok=True)
|
|
318
|
+
for fname in files:
|
|
319
|
+
if os.path.splitext(fname)[1] not in allowed_exts:
|
|
320
|
+
continue
|
|
321
|
+
if os.path.splitext(fname)[1] == ".ctx":
|
|
322
|
+
if ctx_destination:
|
|
323
|
+
print(
|
|
324
|
+
f"Warning: Skipping extra context file '{os.path.join(root, fname)}' because one already exists."
|
|
325
|
+
)
|
|
326
|
+
continue
|
|
327
|
+
dest_path = os.path.join(npc_team_dir, fname)
|
|
328
|
+
ctx_destination = dest_path
|
|
329
|
+
else:
|
|
330
|
+
dest_path = os.path.join(dest_dir, fname)
|
|
331
|
+
if not os.path.exists(dest_path):
|
|
332
|
+
shutil.copy2(os.path.join(root, fname), dest_path)
|
|
333
|
+
copied.append(dest_path)
|
|
334
|
+
return copied
|
|
335
|
+
|
|
336
|
+
applied_templates: List[str] = []
|
|
337
|
+
if parsed_templates:
|
|
338
|
+
for template_name in parsed_templates:
|
|
339
|
+
template_path = _resolve_template_path(template_name)
|
|
340
|
+
if not template_path:
|
|
341
|
+
print(f"Warning: Template '{template_name}' not found in known template directories.")
|
|
342
|
+
continue
|
|
343
|
+
copied = _copy_template(template_path)
|
|
344
|
+
if copied:
|
|
345
|
+
applied_templates.append(template_name)
|
|
346
|
+
|
|
347
|
+
if applied_templates:
|
|
348
|
+
applied_templates = sorted(set(applied_templates))
|
|
349
|
+
if not ctx_destination:
|
|
350
|
+
default_ctx_path = os.path.join(npc_team_dir, "team.ctx")
|
|
211
351
|
default_ctx = {
|
|
212
352
|
'name': '',
|
|
213
|
-
'context' : '',
|
|
353
|
+
'context' : context or '',
|
|
214
354
|
'preferences': '',
|
|
215
355
|
'mcp_servers': '',
|
|
216
356
|
'databases':'',
|
|
217
357
|
'use_global_jinxs': True,
|
|
218
358
|
'forenpc': 'forenpc'
|
|
219
359
|
}
|
|
220
|
-
|
|
360
|
+
if parsed_templates:
|
|
361
|
+
default_ctx['templates'] = parsed_templates
|
|
362
|
+
with open(default_ctx_path, "w") as f:
|
|
221
363
|
yaml.dump(default_ctx, f)
|
|
222
|
-
|
|
364
|
+
ctx_destination = default_ctx_path
|
|
365
|
+
|
|
366
|
+
if applied_templates:
|
|
367
|
+
return (
|
|
368
|
+
f"NPC project initialized in {npc_team_dir} "
|
|
369
|
+
f"using templates: {', '.join(applied_templates)}"
|
|
370
|
+
)
|
|
223
371
|
return f"NPC project initialized in {npc_team_dir}"
|
|
224
372
|
|
|
225
373
|
|
|
@@ -286,6 +434,34 @@ class Jinx:
|
|
|
286
434
|
self.steps = jinx_data.get("steps", [])
|
|
287
435
|
self._source_path = jinx_data.get("_source_path", None)
|
|
288
436
|
|
|
437
|
+
def to_tool_def(self) -> Dict[str, Any]:
|
|
438
|
+
"""Convert this Jinx to an OpenAI-style tool definition."""
|
|
439
|
+
properties = {}
|
|
440
|
+
required = []
|
|
441
|
+
for inp in self.inputs:
|
|
442
|
+
if isinstance(inp, str):
|
|
443
|
+
properties[inp] = {"type": "string", "description": f"Parameter: {inp}"}
|
|
444
|
+
required.append(inp)
|
|
445
|
+
elif isinstance(inp, dict):
|
|
446
|
+
name = list(inp.keys())[0]
|
|
447
|
+
default_val = inp.get(name, "")
|
|
448
|
+
desc = f"Parameter: {name}"
|
|
449
|
+
if default_val != "":
|
|
450
|
+
desc += f" (default: {default_val})"
|
|
451
|
+
properties[name] = {"type": "string", "description": desc}
|
|
452
|
+
return {
|
|
453
|
+
"type": "function",
|
|
454
|
+
"function": {
|
|
455
|
+
"name": self.jinx_name,
|
|
456
|
+
"description": self.description or f"Jinx: {self.jinx_name}",
|
|
457
|
+
"parameters": {
|
|
458
|
+
"type": "object",
|
|
459
|
+
"properties": properties,
|
|
460
|
+
"required": required
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
289
465
|
def render_first_pass(
|
|
290
466
|
self,
|
|
291
467
|
jinja_env_for_macros: Environment,
|
|
@@ -621,100 +797,12 @@ def load_jinxs_from_directory(directory):
|
|
|
621
797
|
|
|
622
798
|
def jinx_to_tool_def(jinx_obj: 'Jinx') -> Dict[str, Any]:
|
|
623
799
|
"""Convert a Jinx instance into an MCP/LLM-compatible tool schema definition."""
|
|
624
|
-
|
|
625
|
-
required: List[str] = []
|
|
626
|
-
for inp in jinx_obj.inputs:
|
|
627
|
-
if isinstance(inp, str):
|
|
628
|
-
properties[inp] = {"type": "string"}
|
|
629
|
-
required.append(inp)
|
|
630
|
-
elif isinstance(inp, dict):
|
|
631
|
-
name = list(inp.keys())[0]
|
|
632
|
-
properties[name] = {"type": "string", "default": inp.get(name, "")}
|
|
633
|
-
required.append(name)
|
|
634
|
-
return {
|
|
635
|
-
"type": "function",
|
|
636
|
-
"function": {
|
|
637
|
-
"name": jinx_obj.jinx_name,
|
|
638
|
-
"description": jinx_obj.description or f"Jinx: {jinx_obj.jinx_name}",
|
|
639
|
-
"parameters": {
|
|
640
|
-
"type": "object",
|
|
641
|
-
"properties": properties,
|
|
642
|
-
"required": required
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
}
|
|
800
|
+
return jinx_obj.to_tool_def()
|
|
646
801
|
|
|
647
802
|
def build_jinx_tool_catalog(jinxs: Dict[str, 'Jinx']) -> Dict[str, Dict[str, Any]]:
|
|
648
803
|
"""Helper to build a name->tool_def catalog from a dict of Jinx objects."""
|
|
649
804
|
return {name: jinx_to_tool_def(jinx_obj) for name, jinx_obj in jinxs.items()}
|
|
650
805
|
|
|
651
|
-
def get_npc_action_space(npc=None, team=None):
|
|
652
|
-
"""Get action space for NPC including memory CRUD and core capabilities"""
|
|
653
|
-
actions = DEFAULT_ACTION_SPACE.copy()
|
|
654
|
-
|
|
655
|
-
if npc:
|
|
656
|
-
core_tools = [
|
|
657
|
-
npc.think_step_by_step,
|
|
658
|
-
]
|
|
659
|
-
if hasattr(npc, "write_code"):
|
|
660
|
-
core_tools.append(npc.write_code)
|
|
661
|
-
|
|
662
|
-
if npc.command_history:
|
|
663
|
-
core_tools.extend([
|
|
664
|
-
npc.search_my_conversations,
|
|
665
|
-
npc.search_my_memories,
|
|
666
|
-
npc.create_memory,
|
|
667
|
-
npc.read_memory,
|
|
668
|
-
npc.update_memory,
|
|
669
|
-
npc.delete_memory,
|
|
670
|
-
npc.search_memories,
|
|
671
|
-
npc.get_all_memories,
|
|
672
|
-
npc.archive_old_memories,
|
|
673
|
-
npc.get_memory_stats
|
|
674
|
-
])
|
|
675
|
-
|
|
676
|
-
if npc.db_conn:
|
|
677
|
-
core_tools.append(npc.query_database)
|
|
678
|
-
|
|
679
|
-
if hasattr(npc, 'tools') and npc.tools:
|
|
680
|
-
core_tools.extend([func for func in npc.tool_map.values() if callable(func)])
|
|
681
|
-
|
|
682
|
-
if core_tools:
|
|
683
|
-
tools_schema, tool_map = auto_tools(core_tools)
|
|
684
|
-
actions.update({
|
|
685
|
-
f"use_{tool.__name__}": {
|
|
686
|
-
"description": f"Use {tool.__name__} capability",
|
|
687
|
-
"handler": tool,
|
|
688
|
-
"context": lambda **_: f"Available as automated capability",
|
|
689
|
-
"output_keys": {"result": {"description": "Tool execution result", "type": "string"}}
|
|
690
|
-
}
|
|
691
|
-
for tool in core_tools
|
|
692
|
-
})
|
|
693
|
-
|
|
694
|
-
if team and hasattr(team, 'npcs') and len(team.npcs) > 1:
|
|
695
|
-
available_npcs = [name for name in team.npcs.keys() if name != (npc.name if npc else None)]
|
|
696
|
-
|
|
697
|
-
def team_aware_handler(command, extracted_data, **kwargs):
|
|
698
|
-
if 'team' not in kwargs or kwargs['team'] is None:
|
|
699
|
-
kwargs['team'] = team
|
|
700
|
-
return agent_pass_handler(command, extracted_data, **kwargs)
|
|
701
|
-
|
|
702
|
-
actions["pass_to_npc"] = {
|
|
703
|
-
"description": "Pass request to another NPC - only when task requires their specific expertise",
|
|
704
|
-
"handler": team_aware_handler,
|
|
705
|
-
"context": lambda npc=npc, team=team, **_: (
|
|
706
|
-
f"Available NPCs: {', '.join(available_npcs)}. "
|
|
707
|
-
f"Only pass when you genuinely cannot complete the task."
|
|
708
|
-
),
|
|
709
|
-
"output_keys": {
|
|
710
|
-
"target_npc": {
|
|
711
|
-
"description": "Name of the NPC to pass the request to",
|
|
712
|
-
"type": "string"
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
return actions
|
|
718
806
|
def extract_jinx_inputs(args: List[str], jinx: Jinx) -> Dict[str, Any]:
|
|
719
807
|
print(f"DEBUG extract_jinx_inputs called with args: {args}")
|
|
720
808
|
print(f"DEBUG jinx.inputs: {jinx.inputs}")
|
|
@@ -867,7 +955,9 @@ class NPC:
|
|
|
867
955
|
self.npc_directory = None
|
|
868
956
|
|
|
869
957
|
self.team = team # Store the team reference (can be None)
|
|
870
|
-
|
|
958
|
+
# Only set jinxs_spec from parameter if it wasn't already set by _load_from_file
|
|
959
|
+
if not hasattr(self, 'jinxs_spec') or jinxs is not None:
|
|
960
|
+
self.jinxs_spec = jinxs or "*" # Store the jinx specification for later loading
|
|
871
961
|
|
|
872
962
|
if tools is not None:
|
|
873
963
|
tools_schema, tool_map = auto_tools(tools)
|
|
@@ -933,10 +1023,29 @@ class NPC:
|
|
|
933
1023
|
print(f"Warning: Jinx '{jinx_item}' not found for NPC '{self.name}' during initial load.")
|
|
934
1024
|
|
|
935
1025
|
self.shared_context = {
|
|
1026
|
+
# Data analysis (guac)
|
|
936
1027
|
"dataframes": {},
|
|
937
1028
|
"current_data": None,
|
|
938
1029
|
"computation_results": [],
|
|
939
|
-
"
|
|
1030
|
+
"locals": {}, # Python exec locals for guac mode
|
|
1031
|
+
|
|
1032
|
+
# Memory
|
|
1033
|
+
"memories": {},
|
|
1034
|
+
|
|
1035
|
+
# MCP tools (corca)
|
|
1036
|
+
"mcp_client": None,
|
|
1037
|
+
"mcp_tools": [],
|
|
1038
|
+
"mcp_tool_map": {},
|
|
1039
|
+
|
|
1040
|
+
# Session tracking
|
|
1041
|
+
"session_input_tokens": 0,
|
|
1042
|
+
"session_output_tokens": 0,
|
|
1043
|
+
"session_cost_usd": 0.0,
|
|
1044
|
+
"turn_count": 0,
|
|
1045
|
+
|
|
1046
|
+
# Mode state
|
|
1047
|
+
"current_mode": "agent",
|
|
1048
|
+
"attachments": [],
|
|
940
1049
|
}
|
|
941
1050
|
|
|
942
1051
|
for key, value in kwargs.items():
|
|
@@ -1715,35 +1824,41 @@ class NPC:
|
|
|
1715
1824
|
jinja_env=self.jinja_env # Pass the NPC's second-pass Jinja env
|
|
1716
1825
|
)
|
|
1717
1826
|
|
|
1718
|
-
if
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1827
|
+
# Log jinx call if we have a command_history with add_jinx_call method
|
|
1828
|
+
if self.command_history is not None and hasattr(self.command_history, 'add_jinx_call'):
|
|
1829
|
+
try:
|
|
1830
|
+
self.command_history.add_jinx_call(
|
|
1831
|
+
triggering_message_id=message_id,
|
|
1832
|
+
conversation_id=conversation_id,
|
|
1833
|
+
jinx_name=jinx_name,
|
|
1834
|
+
jinx_inputs=inputs,
|
|
1835
|
+
jinx_output=result,
|
|
1836
|
+
status="success",
|
|
1837
|
+
error_message=None,
|
|
1838
|
+
duration_ms=None,
|
|
1839
|
+
npc_name=self.name,
|
|
1840
|
+
team_name=team_name,
|
|
1841
|
+
)
|
|
1842
|
+
except Exception:
|
|
1843
|
+
pass # Don't fail jinx execution due to logging error
|
|
1731
1844
|
return result
|
|
1732
1845
|
def check_llm_command(self,
|
|
1733
|
-
command,
|
|
1846
|
+
command,
|
|
1734
1847
|
messages=None,
|
|
1735
1848
|
context=None,
|
|
1736
1849
|
team=None,
|
|
1737
|
-
stream=False
|
|
1850
|
+
stream=False,
|
|
1851
|
+
jinxs=None):
|
|
1738
1852
|
"""Check if a command is for the LLM"""
|
|
1739
1853
|
if context is None:
|
|
1740
1854
|
context = self.shared_context
|
|
1741
|
-
|
|
1855
|
+
|
|
1742
1856
|
if team:
|
|
1743
1857
|
self._current_team = team
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1858
|
+
|
|
1859
|
+
# Use provided jinxs or fall back to NPC's own jinxs
|
|
1860
|
+
jinxs_to_use = jinxs if jinxs is not None else self.jinxs_dict
|
|
1861
|
+
|
|
1747
1862
|
return npy.llm_funcs.check_llm_command(
|
|
1748
1863
|
command,
|
|
1749
1864
|
model=self.model,
|
|
@@ -1753,7 +1868,7 @@ class NPC:
|
|
|
1753
1868
|
messages=self.memory if messages is None else messages,
|
|
1754
1869
|
context=context,
|
|
1755
1870
|
stream=stream,
|
|
1756
|
-
|
|
1871
|
+
jinxs=jinxs_to_use,
|
|
1757
1872
|
)
|
|
1758
1873
|
|
|
1759
1874
|
def handle_agent_pass(self,
|
|
@@ -2170,13 +2285,9 @@ class Team:
|
|
|
2170
2285
|
if 'file_patterns' in ctx_data:
|
|
2171
2286
|
file_cache = self._parse_file_patterns(ctx_data['file_patterns'])
|
|
2172
2287
|
self.shared_context['files'] = file_cache
|
|
2173
|
-
|
|
2174
|
-
self.preferences = ctx_data['preferences']
|
|
2175
|
-
else:
|
|
2176
|
-
self.preferences = []
|
|
2177
|
-
|
|
2288
|
+
# All other keys (including preferences) are treated as generic context
|
|
2178
2289
|
for key, item in ctx_data.items():
|
|
2179
|
-
if key not in ['name', 'mcp_servers', 'databases', 'context', 'file_patterns', 'forenpc', 'model', 'provider', 'api_url', 'env'
|
|
2290
|
+
if key not in ['name', 'mcp_servers', 'databases', 'context', 'file_patterns', 'forenpc', 'model', 'provider', 'api_url', 'env']:
|
|
2180
2291
|
self.shared_context[key] = item
|
|
2181
2292
|
return # Only load the first .ctx file found
|
|
2182
2293
|
|
|
@@ -2356,146 +2467,98 @@ class Team:
|
|
|
2356
2467
|
else:
|
|
2357
2468
|
return None
|
|
2358
2469
|
|
|
2359
|
-
def orchestrate(self, request):
|
|
2470
|
+
def orchestrate(self, request, max_iterations=3):
|
|
2360
2471
|
"""Orchestrate a request through the team"""
|
|
2361
|
-
|
|
2472
|
+
import re
|
|
2473
|
+
from termcolor import colored
|
|
2474
|
+
|
|
2475
|
+
forenpc = self.get_forenpc()
|
|
2362
2476
|
if not forenpc:
|
|
2363
2477
|
return {"error": "No forenpc available to coordinate the team"}
|
|
2364
|
-
|
|
2365
|
-
log_entry(
|
|
2366
|
-
self.name,
|
|
2367
|
-
"orchestration_start",
|
|
2368
|
-
{"request": request}
|
|
2369
|
-
)
|
|
2370
|
-
|
|
2371
|
-
result = forenpc.check_llm_command(request,
|
|
2372
|
-
context=getattr(self, 'context', {}),
|
|
2373
|
-
team = self,
|
|
2374
|
-
)
|
|
2375
|
-
|
|
2376
|
-
while True:
|
|
2377
|
-
completion_prompt= ""
|
|
2378
|
-
if isinstance(result, dict):
|
|
2379
|
-
self.shared_context["execution_history"].append(result)
|
|
2380
|
-
|
|
2381
|
-
if result.get("messages") and result.get("npc_name"):
|
|
2382
|
-
if result["npc_name"] not in self.shared_context["npc_messages"]:
|
|
2383
|
-
self.shared_context["npc_messages"][result["npc_name"]] = []
|
|
2384
|
-
self.shared_context["npc_messages"][result["npc_name"]].extend(
|
|
2385
|
-
result["messages"]
|
|
2386
|
-
)
|
|
2387
|
-
|
|
2388
|
-
completion_prompt += f"""Context:
|
|
2389
|
-
User request '{request}', previous agent
|
|
2390
|
-
|
|
2391
|
-
previous agent returned:
|
|
2392
|
-
{result.get('output')}
|
|
2393
2478
|
|
|
2394
|
-
|
|
2395
|
-
|
|
2479
|
+
print(colored(f"[orchestrate] Starting with forenpc={forenpc.name}, team={self.name}", "cyan"))
|
|
2480
|
+
print(colored(f"[orchestrate] Request: {request[:100]}...", "cyan"))
|
|
2396
2481
|
|
|
2397
|
-
|
|
2482
|
+
# Filter out 'orchestrate' jinx to prevent infinite recursion
|
|
2483
|
+
jinxs_for_orchestration = {k: v for k, v in forenpc.jinxs_dict.items() if k != 'orchestrate'}
|
|
2398
2484
|
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
completion_prompt += f"""
|
|
2406
|
-
|
|
2407
|
-
These are all the members of the team: {', '.join(self.npcs.keys())}
|
|
2408
|
-
|
|
2409
|
-
Therefore, if you are trying to evaluate whether a request was fulfilled relevantly,
|
|
2410
|
-
consider that requests are made to the forenpc: {forenpc.name}
|
|
2411
|
-
and that the forenpc must pass those along to the other npcs.
|
|
2412
|
-
"""
|
|
2413
|
-
completion_prompt += f"""
|
|
2414
|
-
|
|
2415
|
-
Mainly concern yourself with ensuring there are no
|
|
2416
|
-
glaring errors nor fundamental mishaps in the response.
|
|
2417
|
-
Do not consider stylistic hiccups as the answers being
|
|
2418
|
-
irrelevant. By providing responses back to for the user to
|
|
2419
|
-
comment on, they can can more efficiently iterate and resolve any issues by
|
|
2420
|
-
prompting more clearly.
|
|
2421
|
-
natural language itself is very fuzzy so there will always be some level
|
|
2422
|
-
of misunderstanding, but as long as the response is clearly relevant
|
|
2423
|
-
to the input request and along the user's intended direction,
|
|
2424
|
-
it is considered relevant.
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
If there is enough information to begin a fruitful conversation with the user,
|
|
2428
|
-
please consider the request relevant so that we do not
|
|
2429
|
-
arbritarily stall business logic which is more efficiently
|
|
2430
|
-
determined by iterations than through unnecessary pedantry.
|
|
2431
|
-
|
|
2432
|
-
It is more important to get a response to the user
|
|
2433
|
-
than to account for all edge cases, so as long as the response more or less tackles the
|
|
2434
|
-
initial problem to first order, consider it relevant.
|
|
2435
|
-
|
|
2436
|
-
Return a JSON object with:
|
|
2437
|
-
-'relevant' with boolean value
|
|
2438
|
-
-'explanation' for irrelevance with quoted citations in your explanation noting why it is irrelevant to user input must be a single string.
|
|
2439
|
-
Return only the JSON object."""
|
|
2440
|
-
|
|
2441
|
-
completion_check = npy.llm_funcs.get_llm_response(
|
|
2442
|
-
completion_prompt,
|
|
2443
|
-
model=forenpc.model,
|
|
2444
|
-
provider=forenpc.provider,
|
|
2445
|
-
api_key=forenpc.api_key,
|
|
2446
|
-
api_url=forenpc.api_url,
|
|
2447
|
-
npc=forenpc,
|
|
2448
|
-
format="json"
|
|
2485
|
+
try:
|
|
2486
|
+
result = forenpc.check_llm_command(
|
|
2487
|
+
request,
|
|
2488
|
+
context=getattr(self, 'context', {}),
|
|
2489
|
+
team=self,
|
|
2490
|
+
jinxs=jinxs_for_orchestration,
|
|
2449
2491
|
)
|
|
2450
|
-
|
|
2451
|
-
if isinstance(
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2492
|
+
print(colored(f"[orchestrate] Initial result type={type(result)}", "cyan"))
|
|
2493
|
+
if isinstance(result, dict):
|
|
2494
|
+
print(colored(f"[orchestrate] Result keys={list(result.keys())}", "cyan"))
|
|
2495
|
+
if 'error' in result:
|
|
2496
|
+
print(colored(f"[orchestrate] Error in result: {result['error']}", "red"))
|
|
2497
|
+
return result
|
|
2498
|
+
except Exception as e:
|
|
2499
|
+
print(colored(f"[orchestrate] Exception in check_llm_command: {e}", "red"))
|
|
2500
|
+
return {"error": str(e), "output": f"Orchestration failed: {e}"}
|
|
2501
|
+
|
|
2502
|
+
# Check if forenpc mentioned other team members - if so, delegate to them
|
|
2503
|
+
output = ""
|
|
2504
|
+
if isinstance(result, dict):
|
|
2505
|
+
output = result.get('output') or result.get('response') or ""
|
|
2506
|
+
|
|
2507
|
+
print(colored(f"[orchestrate] Output preview: {output[:200] if output else 'EMPTY'}...", "cyan"))
|
|
2508
|
+
|
|
2509
|
+
if output and self.npcs:
|
|
2510
|
+
# Look for @npc_name mentions OR just npc names
|
|
2511
|
+
at_pattern = r'@(\w+)'
|
|
2512
|
+
mentions = re.findall(at_pattern, output)
|
|
2513
|
+
|
|
2514
|
+
# Also check for NPC names mentioned without @ (case insensitive)
|
|
2515
|
+
if not mentions:
|
|
2516
|
+
for npc_name in self.npcs.keys():
|
|
2517
|
+
if npc_name.lower() != forenpc.name.lower():
|
|
2518
|
+
if npc_name.lower() in output.lower():
|
|
2519
|
+
mentions.append(npc_name)
|
|
2520
|
+
break
|
|
2521
|
+
|
|
2522
|
+
print(colored(f"[orchestrate] Found mentions: {mentions}", "cyan"))
|
|
2523
|
+
|
|
2524
|
+
for mentioned in mentions:
|
|
2525
|
+
mentioned_lower = mentioned.lower()
|
|
2526
|
+
if mentioned_lower in self.npcs and mentioned_lower != forenpc.name:
|
|
2527
|
+
target_npc = self.npcs[mentioned_lower]
|
|
2528
|
+
print(colored(f"[orchestrate] Delegating to @{mentioned_lower}", "yellow"))
|
|
2529
|
+
|
|
2530
|
+
try:
|
|
2531
|
+
# Execute the request with the target NPC (exclude orchestrate to prevent loops)
|
|
2532
|
+
target_jinxs = {k: v for k, v in target_npc.jinxs_dict.items() if k != 'orchestrate'}
|
|
2533
|
+
delegate_result = target_npc.check_llm_command(
|
|
2534
|
+
request,
|
|
2535
|
+
context=getattr(self, 'context', {}),
|
|
2536
|
+
team=self,
|
|
2537
|
+
jinxs=target_jinxs,
|
|
2538
|
+
)
|
|
2539
|
+
|
|
2540
|
+
if isinstance(delegate_result, dict):
|
|
2541
|
+
delegate_output = delegate_result.get('output') or delegate_result.get('response') or ""
|
|
2542
|
+
if delegate_output:
|
|
2543
|
+
output = f"[{mentioned_lower}]: {delegate_output}"
|
|
2544
|
+
result = delegate_result
|
|
2545
|
+
print(colored(f"[orchestrate] Got response from {mentioned_lower}", "green"))
|
|
2546
|
+
except Exception as e:
|
|
2547
|
+
print(colored(f"[orchestrate] Delegation to {mentioned_lower} failed: {e}", "red"))
|
|
2548
|
+
|
|
2549
|
+
break # Only delegate to first mentioned NPC
|
|
2550
|
+
|
|
2551
|
+
if isinstance(result, dict):
|
|
2552
|
+
final_output = output if output else str(result)
|
|
2553
|
+
return {
|
|
2554
|
+
"output": final_output,
|
|
2555
|
+
"result": result,
|
|
2556
|
+
}
|
|
2557
|
+
else:
|
|
2558
|
+
return {
|
|
2559
|
+
"output": str(result),
|
|
2560
|
+
"result": result,
|
|
2561
|
+
}
|
|
2499
2562
|
|
|
2500
2563
|
def to_dict(self):
|
|
2501
2564
|
"""Convert team to dictionary representation"""
|