npcpy 1.3.4__py3-none-any.whl → 1.3.5__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/build_funcs.py +288 -0
- npcpy/data/load.py +1 -1
- npcpy/data/web.py +5 -4
- npcpy/gen/image_gen.py +2 -1
- npcpy/gen/response.py +118 -65
- npcpy/gen/world_gen.py +609 -0
- npcpy/llm_funcs.py +173 -271
- npcpy/memory/command_history.py +107 -2
- npcpy/memory/knowledge_graph.py +1 -1
- npcpy/npc_compiler.py +176 -32
- npcpy/npc_sysenv.py +5 -5
- npcpy/serve.py +311 -2
- npcpy/sql/npcsql.py +272 -59
- npcpy/work/browser.py +30 -0
- {npcpy-1.3.4.dist-info → npcpy-1.3.5.dist-info}/METADATA +1 -1
- {npcpy-1.3.4.dist-info → npcpy-1.3.5.dist-info}/RECORD +19 -16
- {npcpy-1.3.4.dist-info → npcpy-1.3.5.dist-info}/WHEEL +0 -0
- {npcpy-1.3.4.dist-info → npcpy-1.3.5.dist-info}/licenses/LICENSE +0 -0
- {npcpy-1.3.4.dist-info → npcpy-1.3.5.dist-info}/top_level.txt +0 -0
npcpy/memory/command_history.py
CHANGED
|
@@ -196,8 +196,19 @@ def init_kg_schema(engine: Engine):
|
|
|
196
196
|
Column('value', Text),
|
|
197
197
|
schema=None
|
|
198
198
|
)
|
|
199
|
-
|
|
200
|
-
|
|
199
|
+
|
|
200
|
+
# NPC version history for rollback support
|
|
201
|
+
npc_versions = Table('npc_versions', metadata,
|
|
202
|
+
Column('id', Integer, primary_key=True, autoincrement=True),
|
|
203
|
+
Column('npc_name', String(255), nullable=False),
|
|
204
|
+
Column('team_path', Text, nullable=False), # path to npc_team directory
|
|
205
|
+
Column('version', Integer, nullable=False),
|
|
206
|
+
Column('content', Text, nullable=False), # full YAML content
|
|
207
|
+
Column('created_at', DateTime, default=datetime.utcnow),
|
|
208
|
+
Column('commit_message', Text), # optional description of change
|
|
209
|
+
schema=None
|
|
210
|
+
)
|
|
211
|
+
|
|
201
212
|
metadata.create_all(engine, checkfirst=True)
|
|
202
213
|
|
|
203
214
|
def load_kg_from_db(engine: Engine, team_name: str, npc_name: str, directory_path: str) -> Dict[str, Any]:
|
|
@@ -402,6 +413,100 @@ def save_kg_to_db(engine: Engine, kg_data: Dict[str, Any], team_name: str, npc_n
|
|
|
402
413
|
except Exception as e:
|
|
403
414
|
print(f"Failed to save KG for scope '({team_name}, {npc_name}, {directory_path})': {e}")
|
|
404
415
|
|
|
416
|
+
|
|
417
|
+
# ============== NPC Version History Functions ==============
|
|
418
|
+
|
|
419
|
+
def save_npc_version(engine: Engine, npc_name: str, team_path: str, content: str, commit_message: str = None) -> int:
|
|
420
|
+
"""Save a new version of an NPC config. Returns the new version number."""
|
|
421
|
+
init_kg_schema(engine) # Ensure table exists
|
|
422
|
+
|
|
423
|
+
with engine.begin() as conn:
|
|
424
|
+
# Get current max version
|
|
425
|
+
result = conn.execute(text("""
|
|
426
|
+
SELECT COALESCE(MAX(version), 0) as max_version
|
|
427
|
+
FROM npc_versions
|
|
428
|
+
WHERE npc_name = :npc_name AND team_path = :team_path
|
|
429
|
+
"""), {"npc_name": npc_name, "team_path": team_path})
|
|
430
|
+
row = result.fetchone()
|
|
431
|
+
new_version = (row[0] if row else 0) + 1
|
|
432
|
+
|
|
433
|
+
# Insert new version
|
|
434
|
+
conn.execute(text("""
|
|
435
|
+
INSERT INTO npc_versions (npc_name, team_path, version, content, created_at, commit_message)
|
|
436
|
+
VALUES (:npc_name, :team_path, :version, :content, :created_at, :commit_message)
|
|
437
|
+
"""), {
|
|
438
|
+
"npc_name": npc_name,
|
|
439
|
+
"team_path": team_path,
|
|
440
|
+
"version": new_version,
|
|
441
|
+
"content": content,
|
|
442
|
+
"created_at": datetime.utcnow(),
|
|
443
|
+
"commit_message": commit_message
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
return new_version
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def get_npc_versions(engine: Engine, npc_name: str, team_path: str) -> List[Dict[str, Any]]:
|
|
450
|
+
"""Get all versions of an NPC config."""
|
|
451
|
+
init_kg_schema(engine)
|
|
452
|
+
|
|
453
|
+
with engine.connect() as conn:
|
|
454
|
+
result = conn.execute(text("""
|
|
455
|
+
SELECT id, version, created_at, commit_message
|
|
456
|
+
FROM npc_versions
|
|
457
|
+
WHERE npc_name = :npc_name AND team_path = :team_path
|
|
458
|
+
ORDER BY version DESC
|
|
459
|
+
"""), {"npc_name": npc_name, "team_path": team_path})
|
|
460
|
+
|
|
461
|
+
return [
|
|
462
|
+
{
|
|
463
|
+
"id": row[0],
|
|
464
|
+
"version": row[1],
|
|
465
|
+
"created_at": row[2].isoformat() if row[2] else None,
|
|
466
|
+
"commit_message": row[3]
|
|
467
|
+
}
|
|
468
|
+
for row in result
|
|
469
|
+
]
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def get_npc_version_content(engine: Engine, npc_name: str, team_path: str, version: int = None) -> Optional[str]:
|
|
473
|
+
"""Get the content of a specific NPC version. If version is None, get latest."""
|
|
474
|
+
init_kg_schema(engine)
|
|
475
|
+
|
|
476
|
+
with engine.connect() as conn:
|
|
477
|
+
if version is None:
|
|
478
|
+
result = conn.execute(text("""
|
|
479
|
+
SELECT content FROM npc_versions
|
|
480
|
+
WHERE npc_name = :npc_name AND team_path = :team_path
|
|
481
|
+
ORDER BY version DESC LIMIT 1
|
|
482
|
+
"""), {"npc_name": npc_name, "team_path": team_path})
|
|
483
|
+
else:
|
|
484
|
+
result = conn.execute(text("""
|
|
485
|
+
SELECT content FROM npc_versions
|
|
486
|
+
WHERE npc_name = :npc_name AND team_path = :team_path AND version = :version
|
|
487
|
+
"""), {"npc_name": npc_name, "team_path": team_path, "version": version})
|
|
488
|
+
|
|
489
|
+
row = result.fetchone()
|
|
490
|
+
return row[0] if row else None
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def rollback_npc_to_version(engine: Engine, npc_name: str, team_path: str, version: int) -> Optional[str]:
|
|
494
|
+
"""Rollback an NPC to a specific version. Returns the content if successful."""
|
|
495
|
+
content = get_npc_version_content(engine, npc_name, team_path, version)
|
|
496
|
+
if content is None:
|
|
497
|
+
return None
|
|
498
|
+
|
|
499
|
+
# Save as a new version with rollback message
|
|
500
|
+
save_npc_version(engine, npc_name, team_path, content, f"Rollback to version {version}")
|
|
501
|
+
|
|
502
|
+
# Write to file
|
|
503
|
+
file_path = os.path.join(team_path, f"{npc_name}.npc")
|
|
504
|
+
with open(file_path, 'w') as f:
|
|
505
|
+
f.write(content)
|
|
506
|
+
|
|
507
|
+
return content
|
|
508
|
+
|
|
509
|
+
|
|
405
510
|
def generate_message_id() -> str:
|
|
406
511
|
return str(uuid.uuid4())
|
|
407
512
|
|
npcpy/memory/knowledge_graph.py
CHANGED
npcpy/npc_compiler.py
CHANGED
|
@@ -411,7 +411,12 @@ class Jinx:
|
|
|
411
411
|
|
|
412
412
|
# _raw_steps will now hold the original, potentially templated, steps definition
|
|
413
413
|
self._raw_steps = list(self.steps)
|
|
414
|
-
|
|
414
|
+
# If steps are already valid dicts (not needing Jinja templating), keep them
|
|
415
|
+
# Otherwise clear for first-pass rendering to populate
|
|
416
|
+
if self.steps and all(isinstance(s, dict) for s in self.steps):
|
|
417
|
+
pass # Keep steps as-is for simple jinxes
|
|
418
|
+
else:
|
|
419
|
+
self.steps = [] # Will be populated after first-pass rendering
|
|
415
420
|
self.parsed_files = {}
|
|
416
421
|
if self.file_context:
|
|
417
422
|
self.parsed_files = self._parse_file_patterns(self.file_context)
|
|
@@ -420,7 +425,8 @@ class Jinx:
|
|
|
420
425
|
jinx_data = load_yaml_file(path)
|
|
421
426
|
if not jinx_data:
|
|
422
427
|
raise ValueError(f"Failed to load jinx from {path}")
|
|
423
|
-
|
|
428
|
+
# Set _source_path in the data so it's preserved after _load_from_data
|
|
429
|
+
jinx_data['_source_path'] = path
|
|
424
430
|
self._load_from_data(jinx_data)
|
|
425
431
|
|
|
426
432
|
|
|
@@ -638,7 +644,8 @@ class Jinx:
|
|
|
638
644
|
extra_globals=extra_globals
|
|
639
645
|
)
|
|
640
646
|
# If an error occurred in a step, propagate it and stop execution
|
|
641
|
-
|
|
647
|
+
output_str = str(context.get("output", ""))
|
|
648
|
+
if "error" in output_str.lower():
|
|
642
649
|
self._log_debug(f"DEBUG: Jinx '{self.jinx_name}' execution stopped due to error in step '{step.get('name', 'unnamed_step')}': {context['output']}")
|
|
643
650
|
break
|
|
644
651
|
|
|
@@ -705,37 +712,54 @@ class Jinx:
|
|
|
705
712
|
|
|
706
713
|
if extra_globals:
|
|
707
714
|
exec_globals.update(extra_globals)
|
|
708
|
-
|
|
709
|
-
|
|
715
|
+
|
|
716
|
+
# Add context values directly as variables so jinx code can use them without Jinja
|
|
717
|
+
exec_globals.update(context)
|
|
718
|
+
|
|
719
|
+
# NOTE: Using same dict for globals and locals because when they're
|
|
720
|
+
# separate, imports end up in locals but nested functions can only see globals.
|
|
721
|
+
# This caused "name 'X' is not defined" errors when functions used imported names.
|
|
722
|
+
exec_locals = exec_globals # Use same namespace so imports are visible in functions
|
|
710
723
|
|
|
711
724
|
try:
|
|
712
725
|
exec(rendered_code, exec_globals, exec_locals)
|
|
713
726
|
except Exception as e:
|
|
714
727
|
error_msg = (
|
|
715
|
-
f"Error executing step '{step_name}': "
|
|
728
|
+
f"Error executing step '{step_name}' in jinx '{self.jinx_name}': "
|
|
716
729
|
f"{type(e).__name__}: {e}"
|
|
717
730
|
)
|
|
731
|
+
print(f"[JINX-ERROR] {error_msg}")
|
|
718
732
|
context['output'] = error_msg
|
|
719
733
|
self._log_debug(error_msg)
|
|
720
734
|
return context
|
|
721
|
-
|
|
735
|
+
|
|
722
736
|
# Update the main context with any variables set in exec_locals
|
|
737
|
+
# But preserve context['output'] if jinx set it via context['output'] = ...
|
|
738
|
+
context_output = context.get("output")
|
|
723
739
|
context.update(exec_locals)
|
|
724
|
-
|
|
725
|
-
|
|
740
|
+
|
|
741
|
+
# If jinx set context['output'] directly, preserve it
|
|
742
|
+
if context_output is not None:
|
|
743
|
+
context["output"] = context_output
|
|
744
|
+
context[step_name] = context_output
|
|
745
|
+
|
|
746
|
+
# Only use exec_locals output if it was explicitly set (not still None from init)
|
|
747
|
+
if "output" in exec_locals and exec_locals["output"] is not None:
|
|
726
748
|
outp = exec_locals["output"]
|
|
727
749
|
context["output"] = outp
|
|
728
750
|
context[step_name] = outp
|
|
729
751
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
752
|
+
# Append to messages if we have output
|
|
753
|
+
final_output = context.get("output")
|
|
754
|
+
if final_output is not None and messages is not None:
|
|
755
|
+
messages.append({
|
|
756
|
+
'role':'assistant',
|
|
757
|
+
'content': (
|
|
758
|
+
f'Jinx {self.jinx_name} step {step_name} '
|
|
759
|
+
f'executed: {final_output}'
|
|
760
|
+
)
|
|
761
|
+
})
|
|
762
|
+
context['messages'] = messages
|
|
739
763
|
|
|
740
764
|
return context
|
|
741
765
|
|
|
@@ -925,6 +949,51 @@ def build_jinx_tool_catalog(jinxs: Dict[str, 'Jinx']) -> Dict[str, Dict[str, Any
|
|
|
925
949
|
"""Helper to build a name->tool_def catalog from a dict of Jinx objects."""
|
|
926
950
|
return {name: jinx_to_tool_def(jinx_obj) for name, jinx_obj in jinxs.items()}
|
|
927
951
|
|
|
952
|
+
def match_jinx_spec_to_names(jinx_spec: str, team_jinxs_dict: Dict[str, 'Jinx'], jinxs_base_dir: str) -> List[str]:
|
|
953
|
+
"""
|
|
954
|
+
Match a jinx spec pattern to actual jinx names from the team's jinxs_dict.
|
|
955
|
+
|
|
956
|
+
Args:
|
|
957
|
+
jinx_spec: A spec like 'lib/core/python', 'lib/computer_use/*', or just 'python'
|
|
958
|
+
team_jinxs_dict: Dict mapping jinx_name -> Jinx object
|
|
959
|
+
jinxs_base_dir: Base directory where team jinxs are stored (e.g., '/path/to/npc_team/jinxs')
|
|
960
|
+
|
|
961
|
+
Returns:
|
|
962
|
+
List of jinx names that match the spec
|
|
963
|
+
"""
|
|
964
|
+
matched_names = []
|
|
965
|
+
|
|
966
|
+
# First, check if it's a direct jinx name match
|
|
967
|
+
if jinx_spec in team_jinxs_dict:
|
|
968
|
+
return [jinx_spec]
|
|
969
|
+
|
|
970
|
+
# Normalize the spec (add .jinx extension if not present, for path matching)
|
|
971
|
+
spec_pattern = jinx_spec
|
|
972
|
+
if not spec_pattern.endswith('.jinx') and not spec_pattern.endswith('*'):
|
|
973
|
+
spec_pattern += '.jinx'
|
|
974
|
+
|
|
975
|
+
# Handle glob patterns
|
|
976
|
+
for jinx_name, jinx_obj in team_jinxs_dict.items():
|
|
977
|
+
source_path = getattr(jinx_obj, '_source_path', None)
|
|
978
|
+
if not source_path:
|
|
979
|
+
continue
|
|
980
|
+
|
|
981
|
+
# Get relative path from jinxs base directory
|
|
982
|
+
try:
|
|
983
|
+
rel_path = os.path.relpath(source_path, jinxs_base_dir)
|
|
984
|
+
except ValueError:
|
|
985
|
+
# Can happen on Windows with different drives
|
|
986
|
+
continue
|
|
987
|
+
|
|
988
|
+
# Match using fnmatch for glob support
|
|
989
|
+
if fnmatch.fnmatch(rel_path, spec_pattern):
|
|
990
|
+
matched_names.append(jinx_name)
|
|
991
|
+
# Also try matching without .jinx for patterns like lib/core/python
|
|
992
|
+
elif fnmatch.fnmatch(rel_path, spec_pattern.replace('.jinx', '') + '.jinx'):
|
|
993
|
+
matched_names.append(jinx_name)
|
|
994
|
+
|
|
995
|
+
return matched_names
|
|
996
|
+
|
|
928
997
|
def extract_jinx_inputs(args: List[str], jinx: Jinx) -> Dict[str, Any]:
|
|
929
998
|
print(f"DEBUG extract_jinx_inputs called with args: {args}")
|
|
930
999
|
print(f"DEBUG jinx.inputs: {jinx.inputs}")
|
|
@@ -1055,7 +1124,11 @@ class NPC:
|
|
|
1055
1124
|
db_conn: Database connection
|
|
1056
1125
|
"""
|
|
1057
1126
|
if not file and not name and not primary_directive:
|
|
1058
|
-
raise ValueError("Either 'file' or 'name' and 'primary_directive' must be provided")
|
|
1127
|
+
raise ValueError("Either 'file' or 'name' and 'primary_directive' must be provided")
|
|
1128
|
+
|
|
1129
|
+
# Set team reference early so _load_from_file can use it for inheritance
|
|
1130
|
+
self.team = team
|
|
1131
|
+
|
|
1059
1132
|
if file:
|
|
1060
1133
|
if file.endswith(".npc"):
|
|
1061
1134
|
self._load_from_file(file)
|
|
@@ -1076,7 +1149,6 @@ class NPC:
|
|
|
1076
1149
|
self.jinxs_directory = None
|
|
1077
1150
|
self.npc_directory = None
|
|
1078
1151
|
|
|
1079
|
-
self.team = team # Store the team reference (can be None)
|
|
1080
1152
|
# Only set jinxs_spec from parameter if it wasn't already set by _load_from_file
|
|
1081
1153
|
if not hasattr(self, 'jinxs_spec') or jinxs is not None:
|
|
1082
1154
|
self.jinxs_spec = jinxs or "*" # Store the jinx specification for later loading
|
|
@@ -1188,12 +1260,39 @@ class NPC:
|
|
|
1188
1260
|
if self.team and hasattr(self.team, 'jinxs_dict') and self.team.jinxs_dict:
|
|
1189
1261
|
self.jinxs_dict.update(self.team.jinxs_dict)
|
|
1190
1262
|
else: # If specific jinxs are requested, try to get them from team
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1263
|
+
if self.team and hasattr(self.team, 'jinxs_dict') and self.team.jinxs_dict:
|
|
1264
|
+
# Determine the jinxs base directory for path matching
|
|
1265
|
+
jinxs_base_dir = None
|
|
1266
|
+
if hasattr(self.team, 'team_path') and self.team.team_path:
|
|
1267
|
+
jinxs_base_dir = os.path.join(self.team.team_path, 'jinxs')
|
|
1268
|
+
|
|
1269
|
+
for jinx_spec in self.jinxs_spec:
|
|
1270
|
+
# Use the helper to match spec patterns (paths, globs) to jinx names
|
|
1271
|
+
if jinxs_base_dir:
|
|
1272
|
+
matched_names = match_jinx_spec_to_names(jinx_spec, self.team.jinxs_dict, jinxs_base_dir)
|
|
1273
|
+
else:
|
|
1274
|
+
# Fallback to direct name match if no base dir
|
|
1275
|
+
matched_names = [jinx_spec] if jinx_spec in self.team.jinxs_dict else []
|
|
1276
|
+
|
|
1277
|
+
for jinx_name in matched_names:
|
|
1278
|
+
if jinx_name in self.team.jinxs_dict:
|
|
1279
|
+
self.jinxs_dict[jinx_name] = self.team.jinxs_dict[jinx_name]
|
|
1280
|
+
|
|
1281
|
+
# Load NPC's own jinxs ONLY if:
|
|
1282
|
+
# 1. The NPC has no team (standalone NPC), OR
|
|
1283
|
+
# 2. The NPC's jinxs directory is different from the team's jinxs directory
|
|
1284
|
+
# This prevents team NPCs with specific jinxs_spec from loading all team jinxs
|
|
1285
|
+
should_load_from_directory = False
|
|
1196
1286
|
if hasattr(self, 'npc_jinxs_directory') and self.npc_jinxs_directory and os.path.exists(self.npc_jinxs_directory):
|
|
1287
|
+
if not self.team:
|
|
1288
|
+
should_load_from_directory = True
|
|
1289
|
+
elif hasattr(self.team, 'team_path') and self.team.team_path:
|
|
1290
|
+
team_jinxs_dir = os.path.join(self.team.team_path, 'jinxs')
|
|
1291
|
+
# Only load if NPC has its own separate jinxs directory
|
|
1292
|
+
if os.path.normpath(self.npc_jinxs_directory) != os.path.normpath(team_jinxs_dir):
|
|
1293
|
+
should_load_from_directory = True
|
|
1294
|
+
|
|
1295
|
+
if should_load_from_directory:
|
|
1197
1296
|
for jinx_obj in load_jinxs_from_directory(self.npc_jinxs_directory):
|
|
1198
1297
|
if jinx_obj.jinx_name not in self.jinxs_dict: # Only add if not already added from team
|
|
1199
1298
|
npc_jinxs_raw_list.append(jinx_obj)
|
|
@@ -1499,10 +1598,23 @@ class NPC:
|
|
|
1499
1598
|
else:
|
|
1500
1599
|
self.jinxs_spec = jinxs_spec
|
|
1501
1600
|
|
|
1601
|
+
# Get model/provider from NPC file, or inherit from team
|
|
1502
1602
|
self.model = npc_data.get("model")
|
|
1503
1603
|
self.provider = npc_data.get("provider")
|
|
1504
1604
|
self.api_url = npc_data.get("api_url")
|
|
1505
1605
|
self.api_key = npc_data.get("api_key")
|
|
1606
|
+
|
|
1607
|
+
# Inherit from team if not set on NPC
|
|
1608
|
+
if self.team:
|
|
1609
|
+
if not self.model and hasattr(self.team, 'model'):
|
|
1610
|
+
self.model = self.team.model
|
|
1611
|
+
if not self.provider and hasattr(self.team, 'provider'):
|
|
1612
|
+
self.provider = self.team.provider
|
|
1613
|
+
if not self.api_url and hasattr(self.team, 'api_url'):
|
|
1614
|
+
self.api_url = self.team.api_url
|
|
1615
|
+
if not self.api_key and hasattr(self.team, 'api_key'):
|
|
1616
|
+
self.api_key = self.team.api_key
|
|
1617
|
+
|
|
1506
1618
|
self.name = npc_data.get("name", self.name)
|
|
1507
1619
|
|
|
1508
1620
|
self.npc_path = file
|
|
@@ -1561,7 +1673,7 @@ class NPC:
|
|
|
1561
1673
|
if use_core_tools:
|
|
1562
1674
|
dynamic_core_tools_list = [
|
|
1563
1675
|
self.think_step_by_step,
|
|
1564
|
-
self.write_code
|
|
1676
|
+
self.write_code,
|
|
1565
1677
|
]
|
|
1566
1678
|
|
|
1567
1679
|
if self.command_history:
|
|
@@ -1707,7 +1819,39 @@ class NPC:
|
|
|
1707
1819
|
response = self.get_llm_response(thinking_prompt, tool_choice = False)
|
|
1708
1820
|
return response.get('response', 'Unable to process thinking request')
|
|
1709
1821
|
|
|
1822
|
+
def write_code(self, task: str, language: str = "python") -> str:
|
|
1823
|
+
"""Write code to accomplish a task.
|
|
1710
1824
|
|
|
1825
|
+
Args:
|
|
1826
|
+
task: Description of what the code should do
|
|
1827
|
+
language: Programming language to use (default: python)
|
|
1828
|
+
|
|
1829
|
+
Returns:
|
|
1830
|
+
The generated code as a string
|
|
1831
|
+
"""
|
|
1832
|
+
code_prompt = f"""Write {language} code to accomplish the following task:
|
|
1833
|
+
|
|
1834
|
+
{task}
|
|
1835
|
+
|
|
1836
|
+
Requirements:
|
|
1837
|
+
- Write clean, well-commented code
|
|
1838
|
+
- Include error handling where appropriate
|
|
1839
|
+
- Make sure the code is complete and runnable
|
|
1840
|
+
- Only output the code, no explanations before or after
|
|
1841
|
+
|
|
1842
|
+
```{language}
|
|
1843
|
+
"""
|
|
1844
|
+
|
|
1845
|
+
response = self.get_llm_response(code_prompt, tool_choice=False)
|
|
1846
|
+
code = response.get('response', '')
|
|
1847
|
+
|
|
1848
|
+
# Clean up the response - extract code if wrapped in markdown
|
|
1849
|
+
if f'```{language}' in code:
|
|
1850
|
+
code = code.split(f'```{language}')[-1]
|
|
1851
|
+
if '```' in code:
|
|
1852
|
+
code = code.split('```')[0]
|
|
1853
|
+
|
|
1854
|
+
return code.strip()
|
|
1711
1855
|
|
|
1712
1856
|
|
|
1713
1857
|
def create_planning_state(self, goal: str) -> Dict[str, Any]:
|
|
@@ -2348,18 +2492,18 @@ class Team:
|
|
|
2348
2492
|
if not os.path.exists(self.team_path):
|
|
2349
2493
|
raise ValueError(f"Team directory not found: {self.team_path}")
|
|
2350
2494
|
|
|
2351
|
-
# 1. Load
|
|
2495
|
+
# 1. Load team context first (model, provider, forenpc name, etc.)
|
|
2496
|
+
self._load_team_context_file()
|
|
2497
|
+
|
|
2498
|
+
# 2. Load all NPCs (they can now inherit team's model/provider)
|
|
2352
2499
|
for filename in os.listdir(self.team_path):
|
|
2353
2500
|
if filename.endswith(".npc"):
|
|
2354
2501
|
npc_path = os.path.join(self.team_path, filename)
|
|
2355
2502
|
# Pass 'self' to NPC constructor for team reference
|
|
2356
2503
|
# Do NOT pass jinxs=... here, as it will be initialized later
|
|
2357
|
-
npc = NPC(npc_path, db_conn=self.db_conn, team=self)
|
|
2504
|
+
npc = NPC(npc_path, db_conn=self.db_conn, team=self)
|
|
2358
2505
|
self.npcs[npc.name] = npc
|
|
2359
|
-
|
|
2360
|
-
# 2. Load team context and determine forenpc name (string)
|
|
2361
|
-
self._load_team_context_file() # This populates self.model, self.provider, self.forenpc_name etc.
|
|
2362
|
-
|
|
2506
|
+
|
|
2363
2507
|
# 3. Resolve and set self.forenpc (NPC object)
|
|
2364
2508
|
if self.forenpc_name and self.forenpc_name in self.npcs:
|
|
2365
2509
|
self.forenpc = self.npcs[self.forenpc_name]
|
npcpy/npc_sysenv.py
CHANGED
|
@@ -848,17 +848,17 @@ The current date and time are : {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
|
|
|
848
848
|
if team_preferences:
|
|
849
849
|
system_message += f"Team preferences: {team_preferences}\n"
|
|
850
850
|
|
|
851
|
-
# Add team members
|
|
851
|
+
# Add team members with their directives
|
|
852
852
|
if hasattr(team, 'npcs') and team.npcs:
|
|
853
853
|
members = []
|
|
854
854
|
for name, member in team.npcs.items():
|
|
855
855
|
if name != npc.name: # Don't list self
|
|
856
856
|
directive = getattr(member, 'primary_directive', '')
|
|
857
|
-
#
|
|
858
|
-
desc = directive.
|
|
859
|
-
members.append(f" - {name}: {desc}")
|
|
857
|
+
# Include full directive (up to 500 chars) for better delegation decisions
|
|
858
|
+
desc = directive[:500].strip() if directive else ''
|
|
859
|
+
members.append(f" - @{name}: {desc}")
|
|
860
860
|
if members:
|
|
861
|
-
system_message += "\nTeam members
|
|
861
|
+
system_message += "\nTeam members available for delegation:\n" + "\n".join(members) + "\n"
|
|
862
862
|
|
|
863
863
|
system_message += """
|
|
864
864
|
IMPORTANT:
|