npcpy 1.0.26__py3-none-any.whl → 1.2.32__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 +0 -7
- npcpy/data/audio.py +16 -99
- npcpy/data/image.py +43 -42
- npcpy/data/load.py +83 -124
- npcpy/data/text.py +28 -28
- npcpy/data/video.py +8 -32
- npcpy/data/web.py +51 -23
- npcpy/ft/diff.py +110 -0
- npcpy/ft/ge.py +115 -0
- npcpy/ft/memory_trainer.py +171 -0
- npcpy/ft/model_ensembler.py +357 -0
- npcpy/ft/rl.py +360 -0
- npcpy/ft/sft.py +248 -0
- npcpy/ft/usft.py +128 -0
- npcpy/gen/audio_gen.py +24 -0
- npcpy/gen/embeddings.py +13 -13
- npcpy/gen/image_gen.py +262 -117
- npcpy/gen/response.py +615 -415
- npcpy/gen/video_gen.py +53 -7
- npcpy/llm_funcs.py +1869 -437
- npcpy/main.py +1 -1
- npcpy/memory/command_history.py +844 -510
- npcpy/memory/kg_vis.py +833 -0
- npcpy/memory/knowledge_graph.py +892 -1845
- npcpy/memory/memory_processor.py +81 -0
- npcpy/memory/search.py +188 -90
- npcpy/mix/debate.py +192 -3
- npcpy/npc_compiler.py +1672 -801
- npcpy/npc_sysenv.py +593 -1266
- npcpy/serve.py +3120 -0
- npcpy/sql/ai_function_tools.py +257 -0
- npcpy/sql/database_ai_adapters.py +186 -0
- npcpy/sql/database_ai_functions.py +163 -0
- npcpy/sql/model_runner.py +19 -19
- npcpy/sql/npcsql.py +706 -507
- npcpy/sql/sql_model_compiler.py +156 -0
- npcpy/tools.py +183 -0
- npcpy/work/plan.py +13 -279
- npcpy/work/trigger.py +3 -3
- npcpy-1.2.32.dist-info/METADATA +803 -0
- npcpy-1.2.32.dist-info/RECORD +54 -0
- npcpy/data/dataframes.py +0 -171
- npcpy/memory/deep_research.py +0 -125
- npcpy/memory/sleep.py +0 -557
- npcpy/modes/_state.py +0 -78
- npcpy/modes/alicanto.py +0 -1075
- npcpy/modes/guac.py +0 -785
- npcpy/modes/mcp_npcsh.py +0 -822
- npcpy/modes/npc.py +0 -213
- npcpy/modes/npcsh.py +0 -1158
- npcpy/modes/plonk.py +0 -409
- npcpy/modes/pti.py +0 -234
- npcpy/modes/serve.py +0 -1637
- npcpy/modes/spool.py +0 -312
- npcpy/modes/wander.py +0 -549
- npcpy/modes/yap.py +0 -572
- npcpy/npc_team/alicanto.npc +0 -2
- npcpy/npc_team/alicanto.png +0 -0
- npcpy/npc_team/assembly_lines/test_pipeline.py +0 -181
- npcpy/npc_team/corca.npc +0 -13
- npcpy/npc_team/foreman.npc +0 -7
- npcpy/npc_team/frederic.npc +0 -6
- npcpy/npc_team/frederic4.png +0 -0
- npcpy/npc_team/guac.png +0 -0
- npcpy/npc_team/jinxs/automator.jinx +0 -18
- npcpy/npc_team/jinxs/bash_executer.jinx +0 -31
- npcpy/npc_team/jinxs/calculator.jinx +0 -11
- npcpy/npc_team/jinxs/edit_file.jinx +0 -96
- npcpy/npc_team/jinxs/file_chat.jinx +0 -14
- npcpy/npc_team/jinxs/gui_controller.jinx +0 -28
- npcpy/npc_team/jinxs/image_generation.jinx +0 -29
- npcpy/npc_team/jinxs/internet_search.jinx +0 -30
- npcpy/npc_team/jinxs/local_search.jinx +0 -152
- npcpy/npc_team/jinxs/npcsh_executor.jinx +0 -31
- npcpy/npc_team/jinxs/python_executor.jinx +0 -8
- npcpy/npc_team/jinxs/screen_cap.jinx +0 -25
- npcpy/npc_team/jinxs/sql_executor.jinx +0 -33
- npcpy/npc_team/kadiefa.npc +0 -3
- npcpy/npc_team/kadiefa.png +0 -0
- npcpy/npc_team/npcsh.ctx +0 -9
- npcpy/npc_team/npcsh_sibiji.png +0 -0
- npcpy/npc_team/plonk.npc +0 -2
- npcpy/npc_team/plonk.png +0 -0
- npcpy/npc_team/plonkjr.npc +0 -2
- npcpy/npc_team/plonkjr.png +0 -0
- npcpy/npc_team/sibiji.npc +0 -5
- npcpy/npc_team/sibiji.png +0 -0
- npcpy/npc_team/spool.png +0 -0
- npcpy/npc_team/templates/analytics/celona.npc +0 -0
- npcpy/npc_team/templates/hr_support/raone.npc +0 -0
- npcpy/npc_team/templates/humanities/eriane.npc +0 -4
- npcpy/npc_team/templates/it_support/lineru.npc +0 -0
- npcpy/npc_team/templates/marketing/slean.npc +0 -4
- npcpy/npc_team/templates/philosophy/maurawa.npc +0 -0
- npcpy/npc_team/templates/sales/turnic.npc +0 -4
- npcpy/npc_team/templates/software/welxor.npc +0 -0
- npcpy/npc_team/yap.png +0 -0
- npcpy/routes.py +0 -958
- npcpy/work/mcp_helpers.py +0 -357
- npcpy/work/mcp_server.py +0 -194
- npcpy-1.0.26.data/data/npcpy/npc_team/alicanto.npc +0 -2
- npcpy-1.0.26.data/data/npcpy/npc_team/alicanto.png +0 -0
- npcpy-1.0.26.data/data/npcpy/npc_team/automator.jinx +0 -18
- npcpy-1.0.26.data/data/npcpy/npc_team/bash_executer.jinx +0 -31
- npcpy-1.0.26.data/data/npcpy/npc_team/calculator.jinx +0 -11
- npcpy-1.0.26.data/data/npcpy/npc_team/celona.npc +0 -0
- npcpy-1.0.26.data/data/npcpy/npc_team/corca.npc +0 -13
- npcpy-1.0.26.data/data/npcpy/npc_team/edit_file.jinx +0 -96
- npcpy-1.0.26.data/data/npcpy/npc_team/eriane.npc +0 -4
- npcpy-1.0.26.data/data/npcpy/npc_team/file_chat.jinx +0 -14
- npcpy-1.0.26.data/data/npcpy/npc_team/foreman.npc +0 -7
- npcpy-1.0.26.data/data/npcpy/npc_team/frederic.npc +0 -6
- npcpy-1.0.26.data/data/npcpy/npc_team/frederic4.png +0 -0
- npcpy-1.0.26.data/data/npcpy/npc_team/guac.png +0 -0
- npcpy-1.0.26.data/data/npcpy/npc_team/gui_controller.jinx +0 -28
- npcpy-1.0.26.data/data/npcpy/npc_team/image_generation.jinx +0 -29
- npcpy-1.0.26.data/data/npcpy/npc_team/internet_search.jinx +0 -30
- npcpy-1.0.26.data/data/npcpy/npc_team/kadiefa.npc +0 -3
- npcpy-1.0.26.data/data/npcpy/npc_team/kadiefa.png +0 -0
- npcpy-1.0.26.data/data/npcpy/npc_team/lineru.npc +0 -0
- npcpy-1.0.26.data/data/npcpy/npc_team/local_search.jinx +0 -152
- npcpy-1.0.26.data/data/npcpy/npc_team/maurawa.npc +0 -0
- npcpy-1.0.26.data/data/npcpy/npc_team/npcsh.ctx +0 -9
- npcpy-1.0.26.data/data/npcpy/npc_team/npcsh_executor.jinx +0 -31
- npcpy-1.0.26.data/data/npcpy/npc_team/npcsh_sibiji.png +0 -0
- npcpy-1.0.26.data/data/npcpy/npc_team/plonk.npc +0 -2
- npcpy-1.0.26.data/data/npcpy/npc_team/plonk.png +0 -0
- npcpy-1.0.26.data/data/npcpy/npc_team/plonkjr.npc +0 -2
- npcpy-1.0.26.data/data/npcpy/npc_team/plonkjr.png +0 -0
- npcpy-1.0.26.data/data/npcpy/npc_team/python_executor.jinx +0 -8
- npcpy-1.0.26.data/data/npcpy/npc_team/raone.npc +0 -0
- npcpy-1.0.26.data/data/npcpy/npc_team/screen_cap.jinx +0 -25
- npcpy-1.0.26.data/data/npcpy/npc_team/sibiji.npc +0 -5
- npcpy-1.0.26.data/data/npcpy/npc_team/sibiji.png +0 -0
- npcpy-1.0.26.data/data/npcpy/npc_team/slean.npc +0 -4
- npcpy-1.0.26.data/data/npcpy/npc_team/spool.png +0 -0
- npcpy-1.0.26.data/data/npcpy/npc_team/sql_executor.jinx +0 -33
- npcpy-1.0.26.data/data/npcpy/npc_team/test_pipeline.py +0 -181
- npcpy-1.0.26.data/data/npcpy/npc_team/turnic.npc +0 -4
- npcpy-1.0.26.data/data/npcpy/npc_team/welxor.npc +0 -0
- npcpy-1.0.26.data/data/npcpy/npc_team/yap.png +0 -0
- npcpy-1.0.26.dist-info/METADATA +0 -827
- npcpy-1.0.26.dist-info/RECORD +0 -139
- npcpy-1.0.26.dist-info/entry_points.txt +0 -11
- /npcpy/{modes → ft}/__init__.py +0 -0
- {npcpy-1.0.26.dist-info → npcpy-1.2.32.dist-info}/WHEEL +0 -0
- {npcpy-1.0.26.dist-info → npcpy-1.2.32.dist-info}/licenses/LICENSE +0 -0
- {npcpy-1.0.26.dist-info → npcpy-1.2.32.dist-info}/top_level.txt +0 -0
npcpy/npc_compiler.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
from email import message
|
|
2
1
|
import os
|
|
3
2
|
from pyexpat.errors import messages
|
|
4
3
|
import yaml
|
|
@@ -13,37 +12,221 @@ from datetime import datetime
|
|
|
13
12
|
import hashlib
|
|
14
13
|
import pathlib
|
|
15
14
|
import fnmatch
|
|
16
|
-
import traceback
|
|
17
15
|
import subprocess
|
|
18
16
|
from typing import Any, Dict, List, Optional, Union
|
|
19
17
|
from jinja2 import Environment, FileSystemLoader, Template, Undefined
|
|
20
18
|
from sqlalchemy import create_engine, text
|
|
21
19
|
import npcpy as npy
|
|
22
|
-
|
|
20
|
+
from npcpy.llm_funcs import DEFAULT_ACTION_SPACE
|
|
21
|
+
from npcpy.tools import auto_tools
|
|
23
22
|
|
|
24
23
|
from npcpy.npc_sysenv import (
|
|
25
24
|
ensure_dirs_exist,
|
|
26
|
-
get_npc_path,
|
|
27
25
|
init_db_tables,
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
get_system_message,
|
|
27
|
+
|
|
30
28
|
)
|
|
31
|
-
from npcpy.memory.command_history import CommandHistory
|
|
29
|
+
from npcpy.memory.command_history import CommandHistory, generate_message_id
|
|
32
30
|
|
|
33
31
|
class SilentUndefined(Undefined):
|
|
34
32
|
def _fail_with_undefined_error(self, *args, **kwargs):
|
|
35
33
|
return ""
|
|
36
34
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
# ---------------------------------------------------------------------------
|
|
35
|
+
import math
|
|
36
|
+
from PIL import Image
|
|
40
37
|
|
|
41
38
|
|
|
39
|
+
def agent_pass_handler(command, extracted_data, **kwargs):
|
|
40
|
+
"""Handler for agent pass action"""
|
|
41
|
+
npc = kwargs.get('npc')
|
|
42
|
+
team = kwargs.get('team')
|
|
43
|
+
if not team and npc and hasattr(npc, '_current_team'):
|
|
44
|
+
team = npc._current_team
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
if not npc or not team:
|
|
48
|
+
return {"messages": kwargs.get('messages', []), "output": f"Error: No NPC ({npc.name if npc else 'None'}) or team ({team.name if team else 'None'}) available for agent pass"}
|
|
49
|
+
|
|
50
|
+
target_npc_name = extracted_data.get('target_npc')
|
|
51
|
+
if not target_npc_name:
|
|
52
|
+
return {"messages": kwargs.get('messages', []), "output": "Error: No target NPC specified"}
|
|
53
|
+
|
|
54
|
+
messages = kwargs.get('messages', [])
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
pass_count = 0
|
|
58
|
+
recent_passes = []
|
|
59
|
+
|
|
60
|
+
for msg in messages[-10:]:
|
|
61
|
+
if 'NOTE: THIS COMMAND HAS BEEN PASSED FROM' in msg.get('content', ''):
|
|
62
|
+
pass_count += 1
|
|
63
|
+
|
|
64
|
+
if 'PASSED FROM' in msg.get('content', ''):
|
|
65
|
+
content = msg.get('content', '')
|
|
66
|
+
if 'PASSED FROM' in content and 'TO YOU' in content:
|
|
67
|
+
parts = content.split('PASSED FROM')[1].split('TO YOU')[0].strip()
|
|
68
|
+
recent_passes.append(parts)
|
|
69
|
+
|
|
42
70
|
|
|
71
|
+
|
|
72
|
+
target_npc = team.get_npc(target_npc_name)
|
|
73
|
+
if not target_npc:
|
|
74
|
+
available_npcs = list(team.npcs.keys()) if hasattr(team, 'npcs') else []
|
|
75
|
+
return {"messages": kwargs.get('messages', []),
|
|
76
|
+
"output": f"Error: NPC '{target_npc_name}' not found in team. Available: {available_npcs}"}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
result = npc.handle_agent_pass(
|
|
81
|
+
target_npc,
|
|
82
|
+
command,
|
|
83
|
+
messages=kwargs.get('messages'),
|
|
84
|
+
context=kwargs.get('context'),
|
|
85
|
+
shared_context=getattr(team, 'shared_context', None),
|
|
86
|
+
stream=kwargs.get('stream', False),
|
|
87
|
+
team=team
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
return result
|
|
43
91
|
|
|
44
92
|
|
|
45
|
-
|
|
46
|
-
|
|
93
|
+
def create_or_replace_table(db_path, table_name, data):
|
|
94
|
+
"""Creates or replaces a table in the SQLite database"""
|
|
95
|
+
conn = sqlite3.connect(os.path.expanduser(db_path))
|
|
96
|
+
try:
|
|
97
|
+
data.to_sql(table_name, conn, if_exists="replace", index=False)
|
|
98
|
+
print(f"Table '{table_name}' created/replaced successfully.")
|
|
99
|
+
return True
|
|
100
|
+
except Exception as e:
|
|
101
|
+
print(f"Error creating/replacing table '{table_name}': {e}")
|
|
102
|
+
return False
|
|
103
|
+
finally:
|
|
104
|
+
conn.close()
|
|
105
|
+
|
|
106
|
+
def find_file_path(filename, search_dirs, suffix=None):
|
|
107
|
+
"""Find a file in multiple directories"""
|
|
108
|
+
if suffix and not filename.endswith(suffix):
|
|
109
|
+
filename += suffix
|
|
110
|
+
|
|
111
|
+
for dir_path in search_dirs:
|
|
112
|
+
file_path = os.path.join(os.path.expanduser(dir_path), filename)
|
|
113
|
+
if os.path.exists(file_path):
|
|
114
|
+
return file_path
|
|
115
|
+
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def get_log_entries(entity_id, entry_type=None, limit=10, db_path="~/npcsh_history.db"):
|
|
121
|
+
"""Get log entries for an NPC or team"""
|
|
122
|
+
db_path = os.path.expanduser(db_path)
|
|
123
|
+
with sqlite3.connect(db_path) as conn:
|
|
124
|
+
query = "SELECT entry_type, content, metadata, timestamp FROM npc_log WHERE entity_id = ?"
|
|
125
|
+
params = [entity_id]
|
|
126
|
+
|
|
127
|
+
if entry_type:
|
|
128
|
+
query += " AND entry_type = ?"
|
|
129
|
+
params.append(entry_type)
|
|
130
|
+
|
|
131
|
+
query += " ORDER BY timestamp DESC LIMIT ?"
|
|
132
|
+
params.append(limit)
|
|
133
|
+
|
|
134
|
+
results = conn.execute(query, params).fetchall()
|
|
135
|
+
|
|
136
|
+
return [
|
|
137
|
+
{
|
|
138
|
+
"entry_type": r[0],
|
|
139
|
+
"content": json.loads(r[1]),
|
|
140
|
+
"metadata": json.loads(r[2]) if r[2] else None,
|
|
141
|
+
"timestamp": r[3]
|
|
142
|
+
}
|
|
143
|
+
for r in results
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def load_yaml_file(file_path):
|
|
148
|
+
"""Load a YAML file with error handling"""
|
|
149
|
+
try:
|
|
150
|
+
with open(os.path.expanduser(file_path), 'r') as f:
|
|
151
|
+
return yaml.safe_load(f)
|
|
152
|
+
except Exception as e:
|
|
153
|
+
print(f"Error loading YAML file {file_path}: {e}")
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
def log_entry(entity_id, entry_type, content, metadata=None, db_path="~/npcsh_history.db"):
|
|
157
|
+
"""Log an entry for an NPC or team"""
|
|
158
|
+
db_path = os.path.expanduser(db_path)
|
|
159
|
+
with sqlite3.connect(db_path) as conn:
|
|
160
|
+
conn.execute(
|
|
161
|
+
"INSERT INTO npc_log (entity_id, entry_type, content, metadata) VALUES (?, ?, ?, ?)",
|
|
162
|
+
(entity_id, entry_type, json.dumps(content), json.dumps(metadata) if metadata else None)
|
|
163
|
+
)
|
|
164
|
+
conn.commit()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def initialize_npc_project(
|
|
169
|
+
directory=None,
|
|
170
|
+
templates=None,
|
|
171
|
+
context=None,
|
|
172
|
+
model=None,
|
|
173
|
+
provider=None,
|
|
174
|
+
) -> str:
|
|
175
|
+
"""Initialize an NPC project"""
|
|
176
|
+
if directory is None:
|
|
177
|
+
directory = os.getcwd()
|
|
178
|
+
|
|
179
|
+
npc_team_dir = os.path.join(directory, "npc_team")
|
|
180
|
+
os.makedirs(npc_team_dir, exist_ok=True)
|
|
181
|
+
|
|
182
|
+
for subdir in ["jinxs",
|
|
183
|
+
"assembly_lines",
|
|
184
|
+
"sql_models",
|
|
185
|
+
"jobs",
|
|
186
|
+
"triggers"]:
|
|
187
|
+
os.makedirs(os.path.join(npc_team_dir, subdir), exist_ok=True)
|
|
188
|
+
|
|
189
|
+
forenpc_path = os.path.join(npc_team_dir, "forenpc.npc")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
if not os.path.exists(forenpc_path):
|
|
194
|
+
|
|
195
|
+
default_npc = {
|
|
196
|
+
"name": "forenpc",
|
|
197
|
+
"primary_directive": "You are the forenpc of an NPC team",
|
|
198
|
+
}
|
|
199
|
+
with open(forenpc_path, "w") as f:
|
|
200
|
+
yaml.dump(default_npc, f)
|
|
201
|
+
ctx_path = os.path.join(npc_team_dir, "team.ctx")
|
|
202
|
+
if not os.path.exists(ctx_path):
|
|
203
|
+
default_ctx = {
|
|
204
|
+
'name': '',
|
|
205
|
+
'context' : '',
|
|
206
|
+
'preferences': '',
|
|
207
|
+
'mcp_servers': '',
|
|
208
|
+
'databases':'',
|
|
209
|
+
'use_global_jinxs': True,
|
|
210
|
+
'forenpc': 'forenpc'
|
|
211
|
+
}
|
|
212
|
+
with open(ctx_path, "w") as f:
|
|
213
|
+
yaml.dump(default_ctx, f)
|
|
214
|
+
|
|
215
|
+
return f"NPC project initialized in {npc_team_dir}"
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def write_yaml_file(file_path, data):
|
|
222
|
+
"""Write data to a YAML file"""
|
|
223
|
+
try:
|
|
224
|
+
with open(os.path.expanduser(file_path), 'w') as f:
|
|
225
|
+
yaml.dump(data, f)
|
|
226
|
+
return True
|
|
227
|
+
except Exception as e:
|
|
228
|
+
print(f"Error writing YAML file {file_path}: {e}")
|
|
229
|
+
return False
|
|
47
230
|
|
|
48
231
|
|
|
49
232
|
class Jinx:
|
|
@@ -81,7 +264,6 @@ class Jinx:
|
|
|
81
264
|
self.inputs = jinx_data.get("inputs", [])
|
|
82
265
|
self.description = jinx_data.get("description", "")
|
|
83
266
|
self.steps = self._parse_steps(jinx_data.get("steps", []))
|
|
84
|
-
|
|
85
267
|
def _parse_steps(self, steps):
|
|
86
268
|
"""Parse steps from jinx definition"""
|
|
87
269
|
parsed_steps = []
|
|
@@ -92,62 +274,71 @@ class Jinx:
|
|
|
92
274
|
"engine": step.get("engine", "natural"),
|
|
93
275
|
"code": step.get("code", "")
|
|
94
276
|
}
|
|
277
|
+
if "mode" in step:
|
|
278
|
+
parsed_step["mode"] = step["mode"]
|
|
95
279
|
parsed_steps.append(parsed_step)
|
|
96
280
|
else:
|
|
97
281
|
raise ValueError(f"Invalid step format: {step}")
|
|
98
282
|
return parsed_steps
|
|
99
|
-
|
|
283
|
+
|
|
100
284
|
def execute(self,
|
|
101
|
-
input_values,
|
|
102
|
-
jinxs_dict,
|
|
103
|
-
jinja_env = None,
|
|
104
|
-
npc = None,
|
|
105
|
-
messages=None
|
|
106
|
-
|
|
285
|
+
input_values: Dict[str, Any],
|
|
286
|
+
jinxs_dict: Dict[str, 'Jinx'],
|
|
287
|
+
jinja_env: Optional[Environment] = None,
|
|
288
|
+
npc: Optional[Any] = None,
|
|
289
|
+
messages: Optional[List[Dict[str, str]]] = None,
|
|
290
|
+
**kwargs: Any):
|
|
291
|
+
"""
|
|
292
|
+
Execute the jinx with given inputs.
|
|
293
|
+
**kwargs can be used to pass 'extra_globals' for the python engine.
|
|
294
|
+
"""
|
|
107
295
|
if jinja_env is None:
|
|
296
|
+
from jinja2 import DictLoader
|
|
108
297
|
jinja_env = Environment(
|
|
109
|
-
loader=
|
|
298
|
+
loader=DictLoader({}),
|
|
110
299
|
undefined=SilentUndefined,
|
|
111
300
|
)
|
|
112
|
-
|
|
113
|
-
context = (npc.shared_context.copy() if npc else {})
|
|
301
|
+
|
|
302
|
+
context = (npc.shared_context.copy() if npc and hasattr(npc, 'shared_context') else {})
|
|
114
303
|
context.update(input_values)
|
|
115
304
|
context.update({
|
|
116
305
|
"jinxs": jinxs_dict,
|
|
117
306
|
"llm_response": None,
|
|
118
|
-
"output": None
|
|
307
|
+
"output": None,
|
|
308
|
+
"messages": messages,
|
|
119
309
|
})
|
|
120
310
|
|
|
121
|
-
#
|
|
311
|
+
# This is the key change: Extract 'extra_globals' from kwargs
|
|
312
|
+
extra_globals = kwargs.get('extra_globals')
|
|
313
|
+
|
|
122
314
|
for i, step in enumerate(self.steps):
|
|
123
315
|
context = self._execute_step(
|
|
124
|
-
step,
|
|
316
|
+
step,
|
|
125
317
|
context,
|
|
126
|
-
jinja_env,
|
|
127
|
-
npc=npc,
|
|
128
|
-
messages=messages,
|
|
129
|
-
|
|
130
|
-
)
|
|
318
|
+
jinja_env,
|
|
319
|
+
npc=npc,
|
|
320
|
+
messages=messages,
|
|
321
|
+
extra_globals=extra_globals # Pass it down to the step executor
|
|
322
|
+
)
|
|
131
323
|
|
|
132
324
|
return context
|
|
133
|
-
|
|
325
|
+
|
|
134
326
|
def _execute_step(self,
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
):
|
|
141
|
-
"""
|
|
327
|
+
step: Dict[str, Any],
|
|
328
|
+
context: Dict[str, Any],
|
|
329
|
+
jinja_env: Environment,
|
|
330
|
+
npc: Optional[Any] = None,
|
|
331
|
+
messages: Optional[List[Dict[str, str]]] = None,
|
|
332
|
+
extra_globals: Optional[Dict[str, Any]] = None):
|
|
333
|
+
"""
|
|
334
|
+
Execute a single step of the jinx.
|
|
335
|
+
"""
|
|
142
336
|
engine = step.get("engine", "natural")
|
|
143
337
|
code = step.get("code", "")
|
|
144
338
|
step_name = step.get("name", "unnamed_step")
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
339
|
+
mode = step.get("mode", "chat")
|
|
148
340
|
|
|
149
341
|
try:
|
|
150
|
-
#print(code)
|
|
151
342
|
template = jinja_env.from_string(code)
|
|
152
343
|
rendered_code = template.render(**context)
|
|
153
344
|
|
|
@@ -159,24 +350,32 @@ class Jinx:
|
|
|
159
350
|
rendered_code = code
|
|
160
351
|
rendered_engine = engine
|
|
161
352
|
|
|
162
|
-
# Execute based on engine type
|
|
163
353
|
if rendered_engine == "natural":
|
|
164
354
|
if rendered_code.strip():
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
355
|
+
if mode == "agent":
|
|
356
|
+
response = npc.get_llm_response(
|
|
357
|
+
rendered_code,
|
|
358
|
+
context=context,
|
|
359
|
+
messages=messages,
|
|
360
|
+
auto_process_tool_calls=True,
|
|
361
|
+
use_core_tools=True
|
|
362
|
+
)
|
|
363
|
+
else:
|
|
364
|
+
response = npc.get_llm_response(
|
|
365
|
+
rendered_code,
|
|
366
|
+
context=context,
|
|
367
|
+
messages=messages,
|
|
368
|
+
)
|
|
369
|
+
|
|
172
370
|
response_text = response.get("response", "")
|
|
173
371
|
context['output'] = response_text
|
|
174
372
|
context["llm_response"] = response_text
|
|
175
373
|
context["results"] = response_text
|
|
176
374
|
context[step_name] = response_text
|
|
177
375
|
context['messages'] = response.get('messages')
|
|
376
|
+
|
|
178
377
|
elif rendered_engine == "python":
|
|
179
|
-
#
|
|
378
|
+
# Base globals available to all python jinxes, defined within the library (npcpy)
|
|
180
379
|
exec_globals = {
|
|
181
380
|
"__builtins__": __builtins__,
|
|
182
381
|
"npc": npc,
|
|
@@ -191,44 +390,57 @@ class Jinx:
|
|
|
191
390
|
"fnmatch": fnmatch,
|
|
192
391
|
"pathlib": pathlib,
|
|
193
392
|
"subprocess": subprocess,
|
|
194
|
-
"get_llm_response": npy.llm_funcs.get_llm_response,
|
|
195
|
-
|
|
196
|
-
|
|
393
|
+
"get_llm_response": npy.llm_funcs.get_llm_response,
|
|
394
|
+
"CommandHistory": CommandHistory, # This is fine, it's part of npcpy
|
|
395
|
+
}
|
|
197
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)
|
|
198
400
|
|
|
199
|
-
# Execute the code
|
|
200
401
|
exec_locals = {}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
|
409
|
+
|
|
204
410
|
context.update(exec_locals)
|
|
205
411
|
|
|
206
|
-
# Handle explicit output
|
|
207
412
|
if "output" in exec_locals:
|
|
208
|
-
|
|
209
|
-
context[
|
|
413
|
+
outp = exec_locals["output"]
|
|
414
|
+
context["output"] = outp
|
|
415
|
+
context[step_name] = outp
|
|
416
|
+
if messages is not None:
|
|
417
|
+
messages.append({'role':'assistant',
|
|
418
|
+
'content': f'Jinx executed with following output: {outp}'})
|
|
419
|
+
context['messages'] = messages
|
|
420
|
+
|
|
210
421
|
else:
|
|
211
|
-
# Handle unknown engine
|
|
212
422
|
context[step_name] = {"error": f"Unsupported engine: {rendered_engine}"}
|
|
213
423
|
|
|
214
424
|
return context
|
|
215
|
-
|
|
216
425
|
def to_dict(self):
|
|
217
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
|
+
|
|
218
438
|
return {
|
|
219
439
|
"jinx_name": self.jinx_name,
|
|
220
440
|
"description": self.description,
|
|
221
441
|
"inputs": self.inputs,
|
|
222
|
-
"steps":
|
|
223
|
-
{
|
|
224
|
-
"name": step.get("name", f"step_{i}"),
|
|
225
|
-
"engine": step.get("engine"),
|
|
226
|
-
"code": step.get("code")
|
|
227
|
-
}
|
|
228
|
-
for i, step in enumerate(self.steps)
|
|
229
|
-
]
|
|
442
|
+
"steps": steps_list
|
|
230
443
|
}
|
|
231
|
-
|
|
232
444
|
def save(self, directory):
|
|
233
445
|
"""Save jinx to file"""
|
|
234
446
|
jinx_path = os.path.join(directory, f"{self.jinx_name}.jinx")
|
|
@@ -238,19 +450,19 @@ class Jinx:
|
|
|
238
450
|
@classmethod
|
|
239
451
|
def from_mcp(cls, mcp_tool):
|
|
240
452
|
"""Convert an MCP tool to NPC jinx format"""
|
|
241
|
-
|
|
453
|
+
|
|
242
454
|
try:
|
|
243
455
|
import inspect
|
|
244
456
|
|
|
245
|
-
|
|
457
|
+
|
|
246
458
|
doc = mcp_tool.__doc__ or ""
|
|
247
459
|
name = mcp_tool.__name__
|
|
248
460
|
signature = inspect.signature(mcp_tool)
|
|
249
461
|
|
|
250
|
-
|
|
462
|
+
|
|
251
463
|
inputs = []
|
|
252
464
|
for param_name, param in signature.parameters.items():
|
|
253
|
-
if param_name != 'self':
|
|
465
|
+
if param_name != 'self':
|
|
254
466
|
param_type = param.annotation if param.annotation != inspect.Parameter.empty else None
|
|
255
467
|
param_default = None if param.default == inspect.Parameter.empty else param.default
|
|
256
468
|
|
|
@@ -260,7 +472,7 @@ class Jinx:
|
|
|
260
472
|
"default": param_default
|
|
261
473
|
})
|
|
262
474
|
|
|
263
|
-
|
|
475
|
+
|
|
264
476
|
jinx_data = {
|
|
265
477
|
"jinx_name": name,
|
|
266
478
|
"description": doc.strip(),
|
|
@@ -270,7 +482,7 @@ class Jinx:
|
|
|
270
482
|
"name": "mcp_function_call",
|
|
271
483
|
"engine": "python",
|
|
272
484
|
"code": f"""
|
|
273
|
-
|
|
485
|
+
|
|
274
486
|
import {mcp_tool.__module__}
|
|
275
487
|
output = {mcp_tool.__module__}.{name}(
|
|
276
488
|
{', '.join([f'{inp["name"]}=context.get("{inp["name"]}")' for inp in inputs])}
|
|
@@ -284,28 +496,189 @@ output = {mcp_tool.__module__}.{name}(
|
|
|
284
496
|
|
|
285
497
|
except:
|
|
286
498
|
pass
|
|
499
|
+
|
|
287
500
|
def load_jinxs_from_directory(directory):
|
|
288
|
-
"""Load all jinxs from a directory"""
|
|
501
|
+
"""Load all jinxs from a directory recursively"""
|
|
289
502
|
jinxs = []
|
|
290
503
|
directory = os.path.expanduser(directory)
|
|
291
504
|
|
|
292
505
|
if not os.path.exists(directory):
|
|
293
506
|
return jinxs
|
|
294
|
-
|
|
295
|
-
for
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
507
|
+
|
|
508
|
+
for root, dirs, files in os.walk(directory):
|
|
509
|
+
for filename in files:
|
|
510
|
+
if filename.endswith(".jinx"):
|
|
511
|
+
try:
|
|
512
|
+
jinx_path = os.path.join(root, filename)
|
|
513
|
+
jinx = Jinx(jinx_path=jinx_path)
|
|
514
|
+
jinxs.append(jinx)
|
|
515
|
+
except Exception as e:
|
|
516
|
+
print(f"Error loading jinx {filename}: {e}")
|
|
303
517
|
|
|
304
518
|
return jinxs
|
|
305
519
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
520
|
+
def get_npc_action_space(npc=None, team=None):
|
|
521
|
+
"""Get action space for NPC including memory CRUD and core capabilities"""
|
|
522
|
+
actions = DEFAULT_ACTION_SPACE.copy()
|
|
523
|
+
|
|
524
|
+
if npc:
|
|
525
|
+
core_tools = [
|
|
526
|
+
npc.think_step_by_step,
|
|
527
|
+
npc.write_code
|
|
528
|
+
]
|
|
529
|
+
|
|
530
|
+
if npc.command_history:
|
|
531
|
+
core_tools.extend([
|
|
532
|
+
npc.search_my_conversations,
|
|
533
|
+
npc.search_my_memories,
|
|
534
|
+
npc.create_memory,
|
|
535
|
+
npc.read_memory,
|
|
536
|
+
npc.update_memory,
|
|
537
|
+
npc.delete_memory,
|
|
538
|
+
npc.search_memories,
|
|
539
|
+
npc.get_all_memories,
|
|
540
|
+
npc.archive_old_memories,
|
|
541
|
+
npc.get_memory_stats
|
|
542
|
+
])
|
|
543
|
+
|
|
544
|
+
if npc.db_conn:
|
|
545
|
+
core_tools.append(npc.query_database)
|
|
546
|
+
|
|
547
|
+
if hasattr(npc, 'tools') and npc.tools:
|
|
548
|
+
core_tools.extend([func for func in npc.tool_map.values() if callable(func)])
|
|
549
|
+
|
|
550
|
+
if core_tools:
|
|
551
|
+
tools_schema, tool_map = auto_tools(core_tools)
|
|
552
|
+
actions.update({
|
|
553
|
+
f"use_{tool.__name__}": {
|
|
554
|
+
"description": f"Use {tool.__name__} capability",
|
|
555
|
+
"handler": tool,
|
|
556
|
+
"context": lambda **_: f"Available as automated capability",
|
|
557
|
+
"output_keys": {"result": {"description": "Tool execution result", "type": "string"}}
|
|
558
|
+
}
|
|
559
|
+
for tool in core_tools
|
|
560
|
+
})
|
|
561
|
+
|
|
562
|
+
if team and hasattr(team, 'npcs') and len(team.npcs) > 1:
|
|
563
|
+
available_npcs = [name for name in team.npcs.keys() if name != (npc.name if npc else None)]
|
|
564
|
+
|
|
565
|
+
def team_aware_handler(command, extracted_data, **kwargs):
|
|
566
|
+
if 'team' not in kwargs or kwargs['team'] is None:
|
|
567
|
+
kwargs['team'] = team
|
|
568
|
+
return agent_pass_handler(command, extracted_data, **kwargs)
|
|
569
|
+
|
|
570
|
+
actions["pass_to_npc"] = {
|
|
571
|
+
"description": "Pass request to another NPC - only when task requires their specific expertise",
|
|
572
|
+
"handler": team_aware_handler,
|
|
573
|
+
"context": lambda npc=npc, team=team, **_: (
|
|
574
|
+
f"Available NPCs: {', '.join(available_npcs)}. "
|
|
575
|
+
f"Only pass when you genuinely cannot complete the task."
|
|
576
|
+
),
|
|
577
|
+
"output_keys": {
|
|
578
|
+
"target_npc": {
|
|
579
|
+
"description": "Name of the NPC to pass the request to",
|
|
580
|
+
"type": "string"
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
return actions
|
|
586
|
+
def extract_jinx_inputs(args: List[str], jinx: Jinx) -> Dict[str, Any]:
|
|
587
|
+
print(f"DEBUG extract_jinx_inputs called with args: {args}")
|
|
588
|
+
print(f"DEBUG jinx.inputs: {jinx.inputs}")
|
|
589
|
+
|
|
590
|
+
inputs = {}
|
|
591
|
+
|
|
592
|
+
flag_mapping = {}
|
|
593
|
+
for input_ in jinx.inputs:
|
|
594
|
+
if isinstance(input_, str):
|
|
595
|
+
flag_mapping[f"-{input_[0]}"] = input_
|
|
596
|
+
flag_mapping[f"--{input_}"] = input_
|
|
597
|
+
elif isinstance(input_, dict):
|
|
598
|
+
key = list(input_.keys())[0]
|
|
599
|
+
flag_mapping[f"-{key[0]}"] = key
|
|
600
|
+
flag_mapping[f"--{key}"] = key
|
|
601
|
+
|
|
602
|
+
if len(jinx.inputs) > 1:
|
|
603
|
+
used_args = set()
|
|
604
|
+
for i, arg in enumerate(args):
|
|
605
|
+
if '=' in arg and arg != '=' and not arg.startswith('-'):
|
|
606
|
+
key, value = arg.split('=', 1)
|
|
607
|
+
key = key.strip().strip("'\"")
|
|
608
|
+
value = value.strip().strip("'\"")
|
|
609
|
+
inputs[key] = value
|
|
610
|
+
used_args.add(i)
|
|
611
|
+
else:
|
|
612
|
+
used_args = set()
|
|
613
|
+
|
|
614
|
+
for i, arg in enumerate(args):
|
|
615
|
+
if i in used_args:
|
|
616
|
+
continue
|
|
617
|
+
|
|
618
|
+
if arg in flag_mapping:
|
|
619
|
+
if i + 1 < len(args) and not args[i + 1].startswith('-'):
|
|
620
|
+
input_name = flag_mapping[arg]
|
|
621
|
+
inputs[input_name] = args[i + 1]
|
|
622
|
+
used_args.add(i)
|
|
623
|
+
used_args.add(i + 1)
|
|
624
|
+
else:
|
|
625
|
+
input_name = flag_mapping[arg]
|
|
626
|
+
inputs[input_name] = True
|
|
627
|
+
used_args.add(i)
|
|
628
|
+
|
|
629
|
+
unused_args = [arg for i, arg in enumerate(args) if i not in used_args]
|
|
630
|
+
|
|
631
|
+
print(f"DEBUG unused_args: {unused_args}")
|
|
632
|
+
|
|
633
|
+
# Find first required input (no default value)
|
|
634
|
+
first_required = None
|
|
635
|
+
for input_ in jinx.inputs:
|
|
636
|
+
if isinstance(input_, str):
|
|
637
|
+
first_required = input_
|
|
638
|
+
break
|
|
639
|
+
|
|
640
|
+
print(f"DEBUG first_required: {first_required}")
|
|
641
|
+
|
|
642
|
+
# Give all unused args to first required input
|
|
643
|
+
if first_required and unused_args:
|
|
644
|
+
inputs[first_required] = ' '.join(unused_args).strip()
|
|
645
|
+
print(f"DEBUG assigned to first_required: {inputs[first_required]}")
|
|
646
|
+
else:
|
|
647
|
+
# Fallback to original behavior
|
|
648
|
+
jinx_input_names = []
|
|
649
|
+
for input_ in jinx.inputs:
|
|
650
|
+
if isinstance(input_, str):
|
|
651
|
+
jinx_input_names.append(input_)
|
|
652
|
+
elif isinstance(input_, dict):
|
|
653
|
+
jinx_input_names.append(list(input_.keys())[0])
|
|
654
|
+
|
|
655
|
+
if len(jinx_input_names) == 1:
|
|
656
|
+
inputs[jinx_input_names[0]] = ' '.join(unused_args).strip()
|
|
657
|
+
else:
|
|
658
|
+
for i, arg in enumerate(unused_args):
|
|
659
|
+
if i < len(jinx_input_names):
|
|
660
|
+
input_name = jinx_input_names[i]
|
|
661
|
+
if input_name not in inputs:
|
|
662
|
+
inputs[input_name] = arg
|
|
663
|
+
|
|
664
|
+
for input_ in jinx.inputs:
|
|
665
|
+
if isinstance(input_, str):
|
|
666
|
+
if input_ not in inputs:
|
|
667
|
+
raise ValueError(f"Missing required input: {input_}")
|
|
668
|
+
elif isinstance(input_, dict):
|
|
669
|
+
key = list(input_.keys())[0]
|
|
670
|
+
default_value = input_[key]
|
|
671
|
+
if key not in inputs:
|
|
672
|
+
inputs[key] = default_value
|
|
673
|
+
|
|
674
|
+
print(f"DEBUG final inputs: {inputs}")
|
|
675
|
+
return inputs
|
|
676
|
+
from npcpy.memory.command_history import load_kg_from_db, save_kg_to_db
|
|
677
|
+
from npcpy.memory.knowledge_graph import kg_initial, kg_evolve_incremental, kg_sleep_process, kg_dream_process
|
|
678
|
+
from npcpy.llm_funcs import get_llm_response, breathe
|
|
679
|
+
import os
|
|
680
|
+
from datetime import datetime
|
|
681
|
+
import json
|
|
309
682
|
|
|
310
683
|
class NPC:
|
|
311
684
|
def __init__(
|
|
@@ -313,6 +686,8 @@ class NPC:
|
|
|
313
686
|
file: str = None,
|
|
314
687
|
name: str = None,
|
|
315
688
|
primary_directive: str = None,
|
|
689
|
+
plain_system_message: bool = False,
|
|
690
|
+
team = None,
|
|
316
691
|
jinxs: list = None,
|
|
317
692
|
tools: list = None,
|
|
318
693
|
model: str = None,
|
|
@@ -321,6 +696,7 @@ class NPC:
|
|
|
321
696
|
api_key: str = None,
|
|
322
697
|
db_conn=None,
|
|
323
698
|
use_global_jinxs=False,
|
|
699
|
+
memory = False,
|
|
324
700
|
**kwargs
|
|
325
701
|
):
|
|
326
702
|
"""
|
|
@@ -351,14 +727,24 @@ class NPC:
|
|
|
351
727
|
self.provider = provider
|
|
352
728
|
self.api_url = api_url
|
|
353
729
|
self.api_key = api_key
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
self.npc_directory = None
|
|
360
|
-
|
|
361
|
-
|
|
730
|
+
|
|
731
|
+
if use_global_jinxs:
|
|
732
|
+
self.jinxs_directory = os.path.expanduser('~/.npcsh/npc_team/jinxs/')
|
|
733
|
+
else:
|
|
734
|
+
self.jinxs_directory = None
|
|
735
|
+
self.npc_directory = None
|
|
736
|
+
|
|
737
|
+
self.team = team
|
|
738
|
+
if tools is not None:
|
|
739
|
+
tools_schema, tool_map = auto_tools(tools)
|
|
740
|
+
self.tools = tools_schema
|
|
741
|
+
self.tool_map = tool_map
|
|
742
|
+
self.tools_schema = tools_schema
|
|
743
|
+
else:
|
|
744
|
+
self.tools = []
|
|
745
|
+
self.tool_map = {}
|
|
746
|
+
self.tools_schema = []
|
|
747
|
+
self.plain_system_message = plain_system_message
|
|
362
748
|
self.use_global_jinxs = use_global_jinxs
|
|
363
749
|
|
|
364
750
|
self.memory_length = 20
|
|
@@ -376,22 +762,25 @@ class NPC:
|
|
|
376
762
|
undefined=SilentUndefined,
|
|
377
763
|
)
|
|
378
764
|
|
|
379
|
-
# Set up database connection
|
|
380
765
|
self.db_conn = db_conn
|
|
766
|
+
|
|
767
|
+
# these 4 get overwritten if the db conn
|
|
768
|
+
self.command_history = None
|
|
769
|
+
self.kg_data = None
|
|
770
|
+
self.tables = None
|
|
771
|
+
self.memory = None
|
|
772
|
+
|
|
381
773
|
if self.db_conn:
|
|
382
774
|
self._setup_db()
|
|
383
775
|
self.command_history = CommandHistory(db=self.db_conn)
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
776
|
+
if memory:
|
|
777
|
+
self.kg_data = self._load_npc_kg()
|
|
778
|
+
self.memory = self.get_memory_context()
|
|
779
|
+
|
|
780
|
+
|
|
390
781
|
|
|
391
|
-
# Load jinxs
|
|
392
782
|
self.jinxs = self._load_npc_jinxs(jinxs or "*")
|
|
393
783
|
|
|
394
|
-
# Set up shared context for NPC
|
|
395
784
|
self.shared_context = {
|
|
396
785
|
"dataframes": {},
|
|
397
786
|
"current_data": None,
|
|
@@ -399,19 +788,245 @@ class NPC:
|
|
|
399
788
|
"memories":{}
|
|
400
789
|
}
|
|
401
790
|
|
|
402
|
-
# Add any additional attributes
|
|
403
791
|
for key, value in kwargs.items():
|
|
404
792
|
setattr(self, key, value)
|
|
405
793
|
|
|
406
794
|
if db_conn is not None:
|
|
407
795
|
init_db_tables()
|
|
796
|
+
|
|
797
|
+
def _load_npc_kg(self):
|
|
798
|
+
"""Load knowledge graph data for this NPC from database"""
|
|
799
|
+
if not self.command_history:
|
|
800
|
+
return None
|
|
801
|
+
|
|
802
|
+
directory_path = os.getcwd()
|
|
803
|
+
team_name = getattr(self.team, 'name', 'default_team') if self.team else 'default_team'
|
|
804
|
+
|
|
805
|
+
kg_data = load_kg_from_db(
|
|
806
|
+
engine=self.command_history.engine,
|
|
807
|
+
team_name=team_name,
|
|
808
|
+
npc_name=self.name,
|
|
809
|
+
directory_path=directory_path
|
|
810
|
+
)
|
|
811
|
+
print('# of facts: ', len(kg_data['facts']))
|
|
812
|
+
print('# of facts: ', len(kg_data['concepts']))
|
|
813
|
+
|
|
814
|
+
if not kg_data.get('facts') and not kg_data.get('concepts'):
|
|
815
|
+
return self._initialize_kg_from_history()
|
|
816
|
+
|
|
817
|
+
return kg_data
|
|
818
|
+
|
|
819
|
+
def _initialize_kg_from_history(self):
|
|
820
|
+
"""Initialize KG from conversation history if no KG exists"""
|
|
821
|
+
if not self.command_history:
|
|
822
|
+
return None
|
|
823
|
+
|
|
824
|
+
recent_messages = self.command_history.get_messages_by_npc(
|
|
825
|
+
self.name,
|
|
826
|
+
n_last=50
|
|
827
|
+
)
|
|
828
|
+
print(f'Recent messages from NPC: {recent_messages[0:10]}')
|
|
829
|
+
|
|
830
|
+
if not recent_messages:
|
|
831
|
+
return {
|
|
832
|
+
"generation": 0,
|
|
833
|
+
"facts": [],
|
|
834
|
+
"concepts": [],
|
|
835
|
+
"concept_links": [],
|
|
836
|
+
"fact_to_concept_links": {},
|
|
837
|
+
"fact_to_fact_links": []
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
content_text = "\n".join([
|
|
841
|
+
msg['content'] for msg in recent_messages
|
|
842
|
+
if msg['role'] == 'user' and isinstance(msg['content'], str)
|
|
843
|
+
])
|
|
844
|
+
|
|
845
|
+
if not content_text.strip():
|
|
846
|
+
return {
|
|
847
|
+
"generation": 0,
|
|
848
|
+
"facts": [],
|
|
849
|
+
"concepts": [],
|
|
850
|
+
"concept_links": [],
|
|
851
|
+
"fact_to_concept_links": {},
|
|
852
|
+
"fact_to_fact_links": []
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
kg_data = kg_initial(
|
|
856
|
+
content_text,
|
|
857
|
+
model=self.model,
|
|
858
|
+
provider=self.provider,
|
|
859
|
+
npc=self,
|
|
860
|
+
context=getattr(self, 'shared_context', {})
|
|
861
|
+
)
|
|
862
|
+
self.kg_data = kg_data
|
|
863
|
+
self._save_kg()
|
|
864
|
+
return kg_data
|
|
865
|
+
|
|
866
|
+
def _save_kg(self):
|
|
867
|
+
"""Save current KG data to database"""
|
|
868
|
+
if not self.kg_data or not self.command_history:
|
|
869
|
+
return False
|
|
870
|
+
|
|
871
|
+
directory_path = os.getcwd()
|
|
872
|
+
team_name = getattr(self.team, 'name', 'default_team') if self.team else 'default_team'
|
|
873
|
+
save_kg_to_db(
|
|
874
|
+
engine=self.command_history.engine,
|
|
875
|
+
kg_data=self.kg_data,
|
|
876
|
+
team_name=team_name,
|
|
877
|
+
npc_name=self.name,
|
|
878
|
+
directory_path=directory_path
|
|
879
|
+
)
|
|
880
|
+
return True
|
|
881
|
+
|
|
882
|
+
def get_memory_context(self):
|
|
883
|
+
"""Get formatted memory context for system prompt"""
|
|
884
|
+
if not self.kg_data:
|
|
885
|
+
return ""
|
|
886
|
+
|
|
887
|
+
context_parts = []
|
|
888
|
+
|
|
889
|
+
recent_facts = self.kg_data.get('facts', [])[-10:]
|
|
890
|
+
if recent_facts:
|
|
891
|
+
context_parts.append("Recent memories:")
|
|
892
|
+
for fact in recent_facts:
|
|
893
|
+
context_parts.append(f"- {fact['statement']}")
|
|
894
|
+
|
|
895
|
+
concepts = self.kg_data.get('concepts', [])
|
|
896
|
+
if concepts:
|
|
897
|
+
concept_names = [c['name'] for c in concepts[:5]]
|
|
898
|
+
context_parts.append(f"Key concepts: {', '.join(concept_names)}")
|
|
899
|
+
|
|
900
|
+
return "\n".join(context_parts)
|
|
901
|
+
|
|
902
|
+
def update_memory(
|
|
903
|
+
self,
|
|
904
|
+
user_input: str,
|
|
905
|
+
assistant_response: str
|
|
906
|
+
):
|
|
907
|
+
"""Update NPC memory from conversation turn using KG evolution"""
|
|
908
|
+
conversation_turn = f"User: {user_input}\nAssistant: {assistant_response}"
|
|
909
|
+
|
|
910
|
+
if not self.kg_data:
|
|
911
|
+
self.kg_data = kg_initial(
|
|
912
|
+
content_text=conversation_turn,
|
|
913
|
+
model=self.model,
|
|
914
|
+
provider=self.provider,
|
|
915
|
+
npc=self
|
|
916
|
+
)
|
|
917
|
+
else:
|
|
918
|
+
self.kg_data, _ = kg_evolve_incremental(
|
|
919
|
+
existing_kg=self.kg_data,
|
|
920
|
+
new_content_text=conversation_turn,
|
|
921
|
+
model=self.model,
|
|
922
|
+
provider=self.provider,
|
|
923
|
+
npc=self,
|
|
924
|
+
get_concepts=True,
|
|
925
|
+
link_concepts_facts=False,
|
|
926
|
+
link_concepts_concepts=False,
|
|
927
|
+
link_facts_facts=False
|
|
928
|
+
)
|
|
929
|
+
|
|
930
|
+
self._save_kg()
|
|
931
|
+
|
|
932
|
+
def enter_tool_use_loop(
|
|
933
|
+
self,
|
|
934
|
+
prompt: str,
|
|
935
|
+
tools: list = None,
|
|
936
|
+
tool_map: dict = None,
|
|
937
|
+
max_iterations: int = 5,
|
|
938
|
+
stream: bool = False
|
|
939
|
+
):
|
|
940
|
+
"""Enter interactive tool use loop for complex tasks"""
|
|
941
|
+
if not tools:
|
|
942
|
+
tools = self.tools
|
|
943
|
+
if not tool_map:
|
|
944
|
+
tool_map = self.tool_map
|
|
945
|
+
|
|
946
|
+
messages = self.memory.copy() if self.memory else []
|
|
947
|
+
messages.append({"role": "user", "content": prompt})
|
|
948
|
+
|
|
949
|
+
for iteration in range(max_iterations):
|
|
950
|
+
response = get_llm_response(
|
|
951
|
+
prompt="",
|
|
952
|
+
model=self.model,
|
|
953
|
+
provider=self.provider,
|
|
954
|
+
npc=self,
|
|
955
|
+
messages=messages,
|
|
956
|
+
tools=tools,
|
|
957
|
+
tool_map=tool_map,
|
|
958
|
+
auto_process_tool_calls=True,
|
|
959
|
+
stream=stream
|
|
960
|
+
)
|
|
961
|
+
|
|
962
|
+
messages = response.get('messages', messages)
|
|
963
|
+
|
|
964
|
+
if not response.get('tool_calls'):
|
|
965
|
+
return {
|
|
966
|
+
"final_response": response.get('response'),
|
|
967
|
+
"messages": messages,
|
|
968
|
+
"iterations": iteration + 1
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
return {
|
|
972
|
+
"final_response": "Max iterations reached",
|
|
973
|
+
"messages": messages,
|
|
974
|
+
"iterations": max_iterations
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
def get_code_response(
|
|
978
|
+
self,
|
|
979
|
+
prompt: str,
|
|
980
|
+
language: str = "python",
|
|
981
|
+
execute: bool = False,
|
|
982
|
+
locals_dict: dict = None
|
|
983
|
+
):
|
|
984
|
+
"""Generate and optionally execute code responses"""
|
|
985
|
+
code_prompt = f"""Generate {language} code for: {prompt}
|
|
986
|
+
|
|
987
|
+
Provide ONLY executable {language} code without explanations.
|
|
988
|
+
Do not include markdown formatting or code blocks.
|
|
989
|
+
Begin directly with the code."""
|
|
990
|
+
|
|
991
|
+
response = get_llm_response(
|
|
992
|
+
prompt=code_prompt,
|
|
993
|
+
model=self.model,
|
|
994
|
+
provider=self.provider,
|
|
995
|
+
npc=self,
|
|
996
|
+
stream=False
|
|
997
|
+
)
|
|
998
|
+
|
|
999
|
+
generated_code = response.get('response', '')
|
|
1000
|
+
|
|
1001
|
+
result = {
|
|
1002
|
+
"code": generated_code,
|
|
1003
|
+
"executed": False,
|
|
1004
|
+
"output": None,
|
|
1005
|
+
"error": None
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
if execute and language == "python":
|
|
1009
|
+
if locals_dict is None:
|
|
1010
|
+
locals_dict = {}
|
|
1011
|
+
|
|
1012
|
+
exec_globals = {"__builtins__": __builtins__}
|
|
1013
|
+
exec_globals.update(locals_dict)
|
|
1014
|
+
|
|
1015
|
+
exec_locals = {}
|
|
1016
|
+
exec(generated_code, exec_globals, exec_locals)
|
|
1017
|
+
|
|
1018
|
+
locals_dict.update(exec_locals)
|
|
1019
|
+
result["executed"] = True
|
|
1020
|
+
result["output"] = exec_locals.get("output", "Code executed successfully")
|
|
1021
|
+
|
|
1022
|
+
return result
|
|
1023
|
+
|
|
408
1024
|
def _load_npc_memory(self):
|
|
1025
|
+
"""Enhanced memory loading that includes KG context"""
|
|
409
1026
|
memory = self.command_history.get_messages_by_npc(self.name, n_last=self.memory_length)
|
|
410
|
-
#import pdb
|
|
411
|
-
#pdb.set_trace()
|
|
412
1027
|
memory = [{'role':mem['role'], 'content':mem['content']} for mem in memory]
|
|
413
|
-
|
|
414
1028
|
return memory
|
|
1029
|
+
|
|
415
1030
|
def _load_from_file(self, file):
|
|
416
1031
|
"""Load NPC configuration from file"""
|
|
417
1032
|
if "~" in file:
|
|
@@ -423,19 +1038,15 @@ class NPC:
|
|
|
423
1038
|
if not npc_data:
|
|
424
1039
|
raise ValueError(f"Failed to load NPC from {file}")
|
|
425
1040
|
|
|
426
|
-
# Extract core fields
|
|
427
1041
|
self.name = npc_data.get("name")
|
|
428
1042
|
if not self.name:
|
|
429
|
-
# Fall back to filename if name not in file
|
|
430
1043
|
self.name = os.path.splitext(os.path.basename(file))[0]
|
|
431
1044
|
|
|
432
1045
|
self.primary_directive = npc_data.get("primary_directive")
|
|
433
1046
|
|
|
434
|
-
# Handle wildcard jinxs specification
|
|
435
1047
|
jinxs_spec = npc_data.get("jinxs", "*")
|
|
436
|
-
|
|
1048
|
+
|
|
437
1049
|
if jinxs_spec == "*":
|
|
438
|
-
# Will be loaded in _load_npc_jinxs
|
|
439
1050
|
self.jinxs_spec = "*"
|
|
440
1051
|
else:
|
|
441
1052
|
self.jinxs_spec = jinxs_spec
|
|
@@ -446,123 +1057,574 @@ class NPC:
|
|
|
446
1057
|
self.api_key = npc_data.get("api_key")
|
|
447
1058
|
self.name = npc_data.get("name", self.name)
|
|
448
1059
|
|
|
449
|
-
# Store path for future reference
|
|
450
1060
|
self.npc_path = file
|
|
451
|
-
|
|
452
|
-
# Set NPC-specific jinxs directory path
|
|
453
1061
|
self.npc_jinxs_directory = os.path.join(os.path.dirname(file), "jinxs")
|
|
1062
|
+
|
|
454
1063
|
def get_system_prompt(self, simple=False):
|
|
455
|
-
|
|
1064
|
+
"""Get system prompt for the NPC"""
|
|
1065
|
+
if simple or self.plain_system_message:
|
|
456
1066
|
return self.primary_directive
|
|
457
1067
|
else:
|
|
458
|
-
return get_system_message(self)
|
|
1068
|
+
return get_system_message(self, team=self.team)
|
|
1069
|
+
|
|
459
1070
|
def _setup_db(self):
|
|
460
1071
|
"""Set up database tables and determine type"""
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
self.tables = result.fetchall()
|
|
480
|
-
self.db_type = "sqlite"
|
|
1072
|
+
dialect = self.db_conn.dialect.name
|
|
1073
|
+
|
|
1074
|
+
with self.db_conn.connect() as conn:
|
|
1075
|
+
if dialect == "postgresql":
|
|
1076
|
+
result = conn.execute(text("""
|
|
1077
|
+
SELECT table_name, obj_description((quote_ident(table_name))::regclass, 'pg_class')
|
|
1078
|
+
FROM information_schema.tables
|
|
1079
|
+
WHERE table_schema='public';
|
|
1080
|
+
"""))
|
|
1081
|
+
self.tables = result.fetchall()
|
|
1082
|
+
self.db_type = "postgres"
|
|
1083
|
+
|
|
1084
|
+
elif dialect == "sqlite":
|
|
1085
|
+
result = conn.execute(text(
|
|
1086
|
+
"SELECT name, sql FROM sqlite_master WHERE type='table';"
|
|
1087
|
+
))
|
|
1088
|
+
self.tables = result.fetchall()
|
|
1089
|
+
self.db_type = "sqlite"
|
|
481
1090
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
1091
|
+
else:
|
|
1092
|
+
print(f"Unsupported DB dialect: {dialect}")
|
|
1093
|
+
self.tables = None
|
|
1094
|
+
self.db_type = None
|
|
486
1095
|
|
|
487
|
-
except Exception as e:
|
|
488
|
-
print(f"Error setting up database: {e}")
|
|
489
|
-
self.tables = None
|
|
490
|
-
self.db_type = None
|
|
491
1096
|
def _load_npc_jinxs(self, jinxs):
|
|
492
1097
|
"""Load and process NPC-specific jinxs"""
|
|
493
1098
|
npc_jinxs = []
|
|
494
1099
|
|
|
495
|
-
# Handle wildcard case - load all jinxs from the jinxs directory
|
|
496
1100
|
if jinxs == "*":
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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
|
+
|
|
505
1109
|
self.jinxs_dict = {jinx.jinx_name: jinx for jinx in npc_jinxs}
|
|
506
|
-
#print(npc_jinxs)
|
|
507
1110
|
return npc_jinxs
|
|
508
|
-
|
|
509
1111
|
|
|
510
1112
|
for jinx in jinxs:
|
|
511
|
-
#need to add a block here for mcp jinxs.
|
|
512
|
-
|
|
513
1113
|
if isinstance(jinx, Jinx):
|
|
514
1114
|
npc_jinxs.append(jinx)
|
|
515
1115
|
elif isinstance(jinx, dict):
|
|
516
1116
|
npc_jinxs.append(Jinx(jinx_data=jinx))
|
|
517
|
-
|
|
518
|
-
# Try to load from file
|
|
1117
|
+
elif isinstance(jinx, str):
|
|
519
1118
|
jinx_path = None
|
|
520
1119
|
jinx_name = jinx
|
|
521
1120
|
if not jinx_name.endswith(".jinx"):
|
|
522
1121
|
jinx_name += ".jinx"
|
|
523
1122
|
|
|
524
|
-
|
|
525
|
-
if hasattr(self, 'jinxs_directory') and os.path.exists(self.jinxs_directory):
|
|
1123
|
+
if hasattr(self, 'jinxs_directory') and self.jinxs_directory and os.path.exists(self.jinxs_directory):
|
|
526
1124
|
candidate_path = os.path.join(self.jinxs_directory, jinx_name)
|
|
527
1125
|
if os.path.exists(candidate_path):
|
|
528
1126
|
jinx_path = candidate_path
|
|
529
1127
|
|
|
530
1128
|
if jinx_path:
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
npc_jinxs.append(jinx_obj)
|
|
534
|
-
except Exception as e:
|
|
535
|
-
print(f"Error loading jinx {jinx_path}: {e}")
|
|
1129
|
+
jinx_obj = Jinx(jinx_path=jinx_path)
|
|
1130
|
+
npc_jinxs.append(jinx_obj)
|
|
536
1131
|
|
|
537
|
-
# Update jinxs dictionary
|
|
538
1132
|
self.jinxs_dict = {jinx.jinx_name: jinx for jinx in npc_jinxs}
|
|
1133
|
+
print(npc_jinxs)
|
|
539
1134
|
return npc_jinxs
|
|
540
|
-
|
|
541
1135
|
def get_llm_response(self,
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
1136
|
+
request,
|
|
1137
|
+
jinxs=None,
|
|
1138
|
+
tools: Optional[list] = None,
|
|
1139
|
+
tool_map: Optional[dict] = None,
|
|
1140
|
+
tool_choice=None,
|
|
1141
|
+
messages=None,
|
|
1142
|
+
auto_process_tool_calls=True,
|
|
1143
|
+
use_core_tools: bool = False,
|
|
1144
|
+
**kwargs):
|
|
1145
|
+
all_candidate_functions = []
|
|
1146
|
+
|
|
1147
|
+
if tools is not None and tool_map is not None:
|
|
1148
|
+
all_candidate_functions.extend([func for func in tool_map.values() if callable(func)])
|
|
1149
|
+
elif hasattr(self, 'tool_map') and self.tool_map:
|
|
1150
|
+
all_candidate_functions.extend([func for func in self.tool_map.values() if callable(func)])
|
|
1151
|
+
|
|
1152
|
+
if use_core_tools:
|
|
1153
|
+
dynamic_core_tools_list = [
|
|
1154
|
+
self.think_step_by_step,
|
|
1155
|
+
self.write_code
|
|
1156
|
+
]
|
|
1157
|
+
|
|
1158
|
+
if self.command_history:
|
|
1159
|
+
dynamic_core_tools_list.extend([
|
|
1160
|
+
self.search_my_conversations,
|
|
1161
|
+
self.search_my_memories,
|
|
1162
|
+
self.create_memory,
|
|
1163
|
+
self.read_memory,
|
|
1164
|
+
self.update_memory,
|
|
1165
|
+
self.delete_memory,
|
|
1166
|
+
self.search_memories,
|
|
1167
|
+
self.get_all_memories,
|
|
1168
|
+
self.archive_old_memories,
|
|
1169
|
+
self.get_memory_stats
|
|
1170
|
+
])
|
|
1171
|
+
|
|
1172
|
+
if self.db_conn:
|
|
1173
|
+
dynamic_core_tools_list.append(self.query_database)
|
|
1174
|
+
|
|
1175
|
+
all_candidate_functions.extend(dynamic_core_tools_list)
|
|
1176
|
+
|
|
1177
|
+
unique_functions = []
|
|
1178
|
+
seen_names = set()
|
|
1179
|
+
for func in all_candidate_functions:
|
|
1180
|
+
if func.__name__ not in seen_names:
|
|
1181
|
+
unique_functions.append(func)
|
|
1182
|
+
seen_names.add(func.__name__)
|
|
1183
|
+
|
|
1184
|
+
final_tools_schema = None
|
|
1185
|
+
final_tool_map_dict = None
|
|
1186
|
+
|
|
1187
|
+
if unique_functions:
|
|
1188
|
+
final_tools_schema, final_tool_map_dict = auto_tools(unique_functions)
|
|
1189
|
+
|
|
1190
|
+
if tool_choice is None:
|
|
1191
|
+
if final_tools_schema:
|
|
1192
|
+
tool_choice = "auto"
|
|
1193
|
+
else:
|
|
1194
|
+
tool_choice = "none"
|
|
1195
|
+
|
|
550
1196
|
response = npy.llm_funcs.get_llm_response(
|
|
551
1197
|
request,
|
|
552
|
-
model=self.model,
|
|
553
|
-
provider=self.provider,
|
|
554
1198
|
npc=self,
|
|
555
1199
|
jinxs=jinxs,
|
|
556
|
-
tools
|
|
1200
|
+
tools=final_tools_schema,
|
|
1201
|
+
tool_map=final_tool_map_dict,
|
|
1202
|
+
tool_choice=tool_choice,
|
|
1203
|
+
auto_process_tool_calls=auto_process_tool_calls,
|
|
557
1204
|
messages=self.memory if messages is None else messages,
|
|
558
1205
|
**kwargs
|
|
559
1206
|
)
|
|
560
|
-
|
|
1207
|
+
|
|
561
1208
|
return response
|
|
562
1209
|
|
|
1210
|
+
|
|
1211
|
+
|
|
1212
|
+
def search_my_conversations(self, query: str, limit: int = 5) -> str:
|
|
1213
|
+
"""Search through this NPC's conversation history for relevant information"""
|
|
1214
|
+
if not self.command_history:
|
|
1215
|
+
return "No conversation history available"
|
|
1216
|
+
|
|
1217
|
+
results = self.command_history.search_conversations(query)
|
|
1218
|
+
|
|
1219
|
+
if not results:
|
|
1220
|
+
return f"No conversations found matching '{query}'"
|
|
1221
|
+
|
|
1222
|
+
formatted_results = []
|
|
1223
|
+
for result in results[:limit]:
|
|
1224
|
+
timestamp = result.get('timestamp', 'Unknown time')
|
|
1225
|
+
content = result.get('content', '')[:200] + ('...' if len(result.get('content', '')) > 200 else '')
|
|
1226
|
+
formatted_results.append(f"[{timestamp}] {content}")
|
|
1227
|
+
|
|
1228
|
+
return f"Found {len(results)} conversations matching '{query}':\n" + "\n".join(formatted_results)
|
|
1229
|
+
|
|
1230
|
+
def search_my_memories(self, query: str, limit: int = 10) -> str:
|
|
1231
|
+
"""Search through this NPC's knowledge graph memories for relevant facts and concepts"""
|
|
1232
|
+
if not self.kg_data:
|
|
1233
|
+
return "No memories available"
|
|
1234
|
+
|
|
1235
|
+
query_lower = query.lower()
|
|
1236
|
+
relevant_facts = []
|
|
1237
|
+
relevant_concepts = []
|
|
1238
|
+
|
|
1239
|
+
for fact in self.kg_data.get('facts', []):
|
|
1240
|
+
if query_lower in fact.get('statement', '').lower():
|
|
1241
|
+
relevant_facts.append(fact['statement'])
|
|
1242
|
+
|
|
1243
|
+
for concept in self.kg_data.get('concepts', []):
|
|
1244
|
+
if query_lower in concept.get('name', '').lower():
|
|
1245
|
+
relevant_concepts.append(concept['name'])
|
|
1246
|
+
|
|
1247
|
+
result_parts = []
|
|
1248
|
+
if relevant_facts:
|
|
1249
|
+
result_parts.append(f"Relevant memories: {'; '.join(relevant_facts[:limit])}")
|
|
1250
|
+
if relevant_concepts:
|
|
1251
|
+
result_parts.append(f"Related concepts: {', '.join(relevant_concepts[:limit])}")
|
|
1252
|
+
|
|
1253
|
+
return "\n".join(result_parts) if result_parts else f"No memories found matching '{query}'"
|
|
1254
|
+
|
|
1255
|
+
def query_database(self, sql_query: str) -> str:
|
|
1256
|
+
"""Execute a SQL query against the available database"""
|
|
1257
|
+
if not self.db_conn:
|
|
1258
|
+
return "No database connection available"
|
|
1259
|
+
|
|
1260
|
+
try:
|
|
1261
|
+
with self.db_conn.connect() as conn:
|
|
1262
|
+
result = conn.execute(text(sql_query))
|
|
1263
|
+
rows = result.fetchall()
|
|
1264
|
+
|
|
1265
|
+
if not rows:
|
|
1266
|
+
return "Query executed successfully but returned no results"
|
|
1267
|
+
|
|
1268
|
+
columns = result.keys()
|
|
1269
|
+
formatted_rows = []
|
|
1270
|
+
for row in rows[:20]:
|
|
1271
|
+
row_dict = dict(zip(columns, row))
|
|
1272
|
+
formatted_rows.append(str(row_dict))
|
|
1273
|
+
|
|
1274
|
+
return f"Query results ({len(rows)} total rows, showing first 20):\n" + "\n".join(formatted_rows)
|
|
1275
|
+
|
|
1276
|
+
except Exception as e:
|
|
1277
|
+
return f"Database query error: {str(e)}"
|
|
1278
|
+
|
|
1279
|
+
def think_step_by_step(self, problem: str) -> str:
|
|
1280
|
+
"""Think through a problem step by step using chain of thought reasoning"""
|
|
1281
|
+
thinking_prompt = f"""Think through this problem step by step:
|
|
1282
|
+
|
|
1283
|
+
{problem}
|
|
1284
|
+
|
|
1285
|
+
Break down your reasoning into clear steps:
|
|
1286
|
+
1. First, I need to understand...
|
|
1287
|
+
2. Then, I should consider...
|
|
1288
|
+
3. Next, I need to...
|
|
1289
|
+
4. Finally, I can conclude...
|
|
1290
|
+
|
|
1291
|
+
Provide your step-by-step analysis.
|
|
1292
|
+
Do not under any circumstances ask for feedback from a user. These thoughts are part of an agentic tool that is letting the agent
|
|
1293
|
+
break down a problem by thinking it through. they will review the results and use them accordingly.
|
|
1294
|
+
|
|
1295
|
+
|
|
1296
|
+
"""
|
|
1297
|
+
|
|
1298
|
+
response = self.get_llm_response(thinking_prompt, tool_choice = False)
|
|
1299
|
+
return response.get('response', 'Unable to process thinking request')
|
|
1300
|
+
|
|
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
|
+
|
|
1409
|
+
|
|
1410
|
+
|
|
1411
|
+
def create_planning_state(self, goal: str) -> Dict[str, Any]:
|
|
1412
|
+
"""Create initial planning state for a goal"""
|
|
1413
|
+
return {
|
|
1414
|
+
"goal": goal,
|
|
1415
|
+
"todos": [],
|
|
1416
|
+
"constraints": [],
|
|
1417
|
+
"facts": [],
|
|
1418
|
+
"mistakes": [],
|
|
1419
|
+
"successes": [],
|
|
1420
|
+
"current_todo_index": 0,
|
|
1421
|
+
"current_subtodo_index": 0,
|
|
1422
|
+
"context_summary": ""
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
|
|
1426
|
+
def generate_todos(self, user_goal: str, planning_state: Dict[str, Any], additional_context: str = "") -> List[Dict[str, Any]]:
|
|
1427
|
+
"""Generate high-level todos for a goal"""
|
|
1428
|
+
prompt = f"""
|
|
1429
|
+
You are a high-level project planner. Structure tasks logically:
|
|
1430
|
+
1. Understand current state
|
|
1431
|
+
2. Make required changes
|
|
1432
|
+
3. Verify changes work
|
|
1433
|
+
|
|
1434
|
+
User goal: {user_goal}
|
|
1435
|
+
{additional_context}
|
|
1436
|
+
|
|
1437
|
+
Generate 3-5 todos to accomplish this goal. Use specific actionable language.
|
|
1438
|
+
Each todo should be independent where possible and focused on a single component.
|
|
1439
|
+
|
|
1440
|
+
Return JSON:
|
|
1441
|
+
{{
|
|
1442
|
+
"todos": [
|
|
1443
|
+
{{"description": "todo description", "estimated_complexity": "simple|medium|complex"}},
|
|
1444
|
+
...
|
|
1445
|
+
]
|
|
1446
|
+
}}
|
|
1447
|
+
"""
|
|
1448
|
+
|
|
1449
|
+
response = self.get_llm_response(prompt, format="json", tool_choice=False)
|
|
1450
|
+
todos_data = response.get("response", {}).get("todos", [])
|
|
1451
|
+
return todos_data
|
|
1452
|
+
|
|
1453
|
+
def should_break_down_todo(self, todo: Dict[str, Any]) -> bool:
|
|
1454
|
+
"""Ask LLM if a todo needs breakdown"""
|
|
1455
|
+
prompt = f"""
|
|
1456
|
+
Todo: {todo['description']}
|
|
1457
|
+
Complexity: {todo.get('estimated_complexity', 'unknown')}
|
|
1458
|
+
|
|
1459
|
+
Should this be broken into smaller steps? Consider:
|
|
1460
|
+
- Is it complex enough to warrant breakdown?
|
|
1461
|
+
- Would breakdown make execution clearer?
|
|
1462
|
+
- Are there multiple distinct steps?
|
|
1463
|
+
|
|
1464
|
+
Return JSON: {{"should_break_down": true/false, "reason": "explanation"}}
|
|
1465
|
+
"""
|
|
1466
|
+
|
|
1467
|
+
response = self.get_llm_response(prompt, format="json", tool_choice=False)
|
|
1468
|
+
result = response.get("response", {})
|
|
1469
|
+
return result.get("should_break_down", False)
|
|
1470
|
+
|
|
1471
|
+
def generate_subtodos(self, todo: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
1472
|
+
"""Generate atomic subtodos for a complex todo"""
|
|
1473
|
+
prompt = f"""
|
|
1474
|
+
Parent todo: {todo['description']}
|
|
1475
|
+
|
|
1476
|
+
Break this into atomic, executable subtodos. Each should be:
|
|
1477
|
+
- A single, concrete action
|
|
1478
|
+
- Executable in one step
|
|
1479
|
+
- Clear and unambiguous
|
|
1480
|
+
|
|
1481
|
+
Return JSON:
|
|
1482
|
+
{{
|
|
1483
|
+
"subtodos": [
|
|
1484
|
+
{{"description": "subtodo description", "type": "action|verification|analysis"}},
|
|
1485
|
+
...
|
|
1486
|
+
]
|
|
1487
|
+
}}
|
|
1488
|
+
"""
|
|
1489
|
+
|
|
1490
|
+
response = self.get_llm_response(prompt, format="json")
|
|
1491
|
+
return response.get("response", {}).get("subtodos", [])
|
|
1492
|
+
|
|
1493
|
+
def execute_planning_item(self, item: Dict[str, Any], planning_state: Dict[str, Any], context: str = "") -> Dict[str, Any]:
|
|
1494
|
+
"""Execute a single planning item (todo or subtodo)"""
|
|
1495
|
+
context_summary = self.get_planning_context_summary(planning_state)
|
|
1496
|
+
|
|
1497
|
+
command = f"""
|
|
1498
|
+
Current context:
|
|
1499
|
+
{context_summary}
|
|
1500
|
+
{context}
|
|
1501
|
+
|
|
1502
|
+
Execute this task: {item['description']}
|
|
1503
|
+
|
|
1504
|
+
Constraints to follow:
|
|
1505
|
+
{chr(10).join([f"- {c}" for c in planning_state.get('constraints', [])])}
|
|
1506
|
+
"""
|
|
1507
|
+
|
|
1508
|
+
result = self.check_llm_command(
|
|
1509
|
+
command,
|
|
1510
|
+
context=self.shared_context,
|
|
1511
|
+
stream=False
|
|
1512
|
+
)
|
|
1513
|
+
|
|
1514
|
+
return result
|
|
1515
|
+
|
|
1516
|
+
def get_planning_context_summary(self, planning_state: Dict[str, Any]) -> str:
|
|
1517
|
+
"""Get lightweight context for planning prompts"""
|
|
1518
|
+
context = []
|
|
1519
|
+
facts = planning_state.get('facts', [])
|
|
1520
|
+
mistakes = planning_state.get('mistakes', [])
|
|
1521
|
+
successes = planning_state.get('successes', [])
|
|
1522
|
+
|
|
1523
|
+
if facts:
|
|
1524
|
+
context.append(f"Facts: {'; '.join(facts[:5])}")
|
|
1525
|
+
if mistakes:
|
|
1526
|
+
context.append(f"Recent mistakes: {'; '.join(mistakes[-3:])}")
|
|
1527
|
+
if successes:
|
|
1528
|
+
context.append(f"Recent successes: {'; '.join(successes[-3:])}")
|
|
1529
|
+
return "\n".join(context)
|
|
1530
|
+
|
|
1531
|
+
|
|
1532
|
+
def compress_planning_state(self, messages):
|
|
1533
|
+
if isinstance(messages, list):
|
|
1534
|
+
from npcpy.llm_funcs import breathe, get_facts
|
|
1535
|
+
|
|
1536
|
+
conversation_summary = breathe(messages=messages, npc=self)
|
|
1537
|
+
summary_data = conversation_summary.get('output', '')
|
|
1538
|
+
|
|
1539
|
+
conversation_text = "\n".join([msg['content'] for msg in messages])
|
|
1540
|
+
extracted_facts = get_facts(conversation_text, model=self.model, provider=self.provider, npc=self)
|
|
1541
|
+
|
|
1542
|
+
user_inputs = [msg['content'] for msg in messages if msg.get('role') == 'user']
|
|
1543
|
+
assistant_outputs = [msg['content'] for msg in messages if msg.get('role') == 'assistant']
|
|
1544
|
+
|
|
1545
|
+
planning_state = {
|
|
1546
|
+
"goal": summary_data,
|
|
1547
|
+
"facts": [fact['statement'] if isinstance(fact, dict) else str(fact) for fact in extracted_facts[-10:]],
|
|
1548
|
+
"successes": [output[:100] for output in assistant_outputs[-5:]],
|
|
1549
|
+
"mistakes": [],
|
|
1550
|
+
"todos": user_inputs[-3:],
|
|
1551
|
+
"constraints": []
|
|
1552
|
+
}
|
|
1553
|
+
else:
|
|
1554
|
+
planning_state = messages
|
|
1555
|
+
|
|
1556
|
+
todos = planning_state.get('todos', [])
|
|
1557
|
+
current_index = planning_state.get('current_todo_index', 0)
|
|
1558
|
+
|
|
1559
|
+
if todos and current_index < len(todos):
|
|
1560
|
+
current_focus = todos[current_index].get('description', todos[current_index]) if isinstance(todos[current_index], dict) else str(todos[current_index])
|
|
1561
|
+
else:
|
|
1562
|
+
current_focus = 'No current task'
|
|
1563
|
+
|
|
1564
|
+
compressed = {
|
|
1565
|
+
"goal": planning_state.get("goal", ""),
|
|
1566
|
+
"progress": f"{len(planning_state.get('successes', []))}/{len(todos)} todos completed",
|
|
1567
|
+
"context": self.get_planning_context_summary(planning_state),
|
|
1568
|
+
"current_focus": current_focus
|
|
1569
|
+
}
|
|
1570
|
+
return json.dumps(compressed, indent=2)
|
|
1571
|
+
|
|
1572
|
+
def decompress_planning_state(self, compressed_state: str) -> Dict[str, Any]:
|
|
1573
|
+
"""Restore planning state from compressed string"""
|
|
1574
|
+
try:
|
|
1575
|
+
data = json.loads(compressed_state)
|
|
1576
|
+
return {
|
|
1577
|
+
"goal": data.get("goal", ""),
|
|
1578
|
+
"todos": [],
|
|
1579
|
+
"constraints": [],
|
|
1580
|
+
"facts": [],
|
|
1581
|
+
"mistakes": [],
|
|
1582
|
+
"successes": [],
|
|
1583
|
+
"current_todo_index": 0,
|
|
1584
|
+
"current_subtodo_index": 0,
|
|
1585
|
+
"compressed_context": data.get("context", "")
|
|
1586
|
+
}
|
|
1587
|
+
except json.JSONDecodeError:
|
|
1588
|
+
return self.create_planning_state("")
|
|
1589
|
+
|
|
1590
|
+
def run_planning_loop(self, user_goal: str, interactive: bool = True) -> Dict[str, Any]:
|
|
1591
|
+
"""Run the full planning loop for a goal"""
|
|
1592
|
+
planning_state = self.create_planning_state(user_goal)
|
|
1593
|
+
|
|
1594
|
+
todos = self.generate_todos(user_goal, planning_state)
|
|
1595
|
+
planning_state["todos"] = todos
|
|
1596
|
+
|
|
1597
|
+
for i, todo in enumerate(todos):
|
|
1598
|
+
planning_state["current_todo_index"] = i
|
|
1599
|
+
|
|
1600
|
+
if self.should_break_down_todo(todo):
|
|
1601
|
+
subtodos = self.generate_subtodos(todo)
|
|
1602
|
+
|
|
1603
|
+
for j, subtodo in enumerate(subtodos):
|
|
1604
|
+
planning_state["current_subtodo_index"] = j
|
|
1605
|
+
result = self.execute_planning_item(subtodo, planning_state)
|
|
1606
|
+
|
|
1607
|
+
if result.get("output"):
|
|
1608
|
+
planning_state["successes"].append(f"Completed: {subtodo['description']}")
|
|
1609
|
+
else:
|
|
1610
|
+
planning_state["mistakes"].append(f"Failed: {subtodo['description']}")
|
|
1611
|
+
else:
|
|
1612
|
+
result = self.execute_planning_item(todo, planning_state)
|
|
1613
|
+
|
|
1614
|
+
if result.get("output"):
|
|
1615
|
+
planning_state["successes"].append(f"Completed: {todo['description']}")
|
|
1616
|
+
else:
|
|
1617
|
+
planning_state["mistakes"].append(f"Failed: {todo['description']}")
|
|
1618
|
+
|
|
1619
|
+
return {
|
|
1620
|
+
"planning_state": planning_state,
|
|
1621
|
+
"compressed_state": self.compress_planning_state(planning_state),
|
|
1622
|
+
"summary": f"Completed {len(planning_state['successes'])} tasks for goal: {user_goal}"
|
|
1623
|
+
}
|
|
1624
|
+
|
|
563
1625
|
def execute_jinx(self, jinx_name, inputs, conversation_id=None, message_id=None, team_name=None):
|
|
564
1626
|
"""Execute a jinx by name"""
|
|
565
|
-
|
|
1627
|
+
|
|
566
1628
|
if jinx_name in self.jinxs_dict:
|
|
567
1629
|
jinx = self.jinxs_dict[jinx_name]
|
|
568
1630
|
elif jinx_name in self.jinxs_dict:
|
|
@@ -590,73 +1652,78 @@ class NPC:
|
|
|
590
1652
|
team_name=team_name,
|
|
591
1653
|
)
|
|
592
1654
|
return result
|
|
593
|
-
|
|
1655
|
+
|
|
594
1656
|
def check_llm_command(self,
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
1657
|
+
command,
|
|
1658
|
+
messages=None,
|
|
1659
|
+
context=None,
|
|
1660
|
+
team=None,
|
|
1661
|
+
stream=False):
|
|
600
1662
|
"""Check if a command is for the LLM"""
|
|
601
1663
|
if context is None:
|
|
602
|
-
context = self.shared_context
|
|
603
|
-
|
|
1664
|
+
context = self.shared_context
|
|
1665
|
+
|
|
1666
|
+
if team:
|
|
1667
|
+
self._current_team = team
|
|
1668
|
+
|
|
1669
|
+
actions = get_npc_action_space(npc=self, team=team)
|
|
1670
|
+
|
|
604
1671
|
return npy.llm_funcs.check_llm_command(
|
|
605
1672
|
command,
|
|
606
1673
|
model=self.model,
|
|
607
1674
|
provider=self.provider,
|
|
1675
|
+
npc=self,
|
|
608
1676
|
team=team,
|
|
609
|
-
messages=messages,
|
|
1677
|
+
messages=self.memory if messages is None else messages,
|
|
610
1678
|
context=context,
|
|
611
|
-
stream=stream
|
|
1679
|
+
stream=stream,
|
|
1680
|
+
actions=actions
|
|
612
1681
|
)
|
|
613
1682
|
|
|
614
1683
|
def handle_agent_pass(self,
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
1684
|
+
npc_to_pass,
|
|
1685
|
+
command,
|
|
1686
|
+
messages=None,
|
|
1687
|
+
context=None,
|
|
1688
|
+
shared_context=None,
|
|
1689
|
+
stream=False,
|
|
1690
|
+
team=None):
|
|
621
1691
|
"""Pass a command to another NPC"""
|
|
622
1692
|
print('handling agent pass')
|
|
623
1693
|
if isinstance(npc_to_pass, NPC):
|
|
624
1694
|
target_npc = npc_to_pass
|
|
625
1695
|
else:
|
|
626
|
-
|
|
627
|
-
try:
|
|
628
|
-
npc_path = get_npc_path(npc_to_pass, "~/npcsh_history.db")
|
|
629
|
-
if not npc_path:
|
|
630
|
-
return {"error": f"NPC '{npc_to_pass}' not found"}
|
|
631
|
-
|
|
632
|
-
target_npc = NPC(npc_path, db_conn=self.db_conn)
|
|
633
|
-
except Exception as e:
|
|
634
|
-
return {"error": f"Error loading NPC '{npc_to_pass}': {e}"}
|
|
1696
|
+
return {"error": "Invalid NPC to pass command to"}
|
|
635
1697
|
|
|
636
|
-
# Update shared context
|
|
637
1698
|
if shared_context is not None:
|
|
638
|
-
self.shared_context
|
|
1699
|
+
self.shared_context.update(shared_context)
|
|
1700
|
+
target_npc.shared_context.update(shared_context)
|
|
639
1701
|
|
|
640
|
-
# Add a note that this command was passed from another NPC
|
|
641
1702
|
updated_command = (
|
|
642
1703
|
command
|
|
643
1704
|
+ "\n\n"
|
|
644
1705
|
+ f"NOTE: THIS COMMAND HAS BEEN PASSED FROM {self.name} TO YOU, {target_npc.name}.\n"
|
|
645
1706
|
+ "PLEASE CHOOSE ONE OF THE OTHER OPTIONS WHEN RESPONDING."
|
|
646
1707
|
)
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
return target_npc.check_llm_command(
|
|
1708
|
+
|
|
1709
|
+
result = target_npc.check_llm_command(
|
|
650
1710
|
updated_command,
|
|
651
1711
|
messages=messages,
|
|
652
|
-
context=
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
1712
|
+
context=target_npc.shared_context,
|
|
1713
|
+
team=team,
|
|
1714
|
+
stream=stream
|
|
656
1715
|
)
|
|
657
|
-
|
|
1716
|
+
if isinstance(result, dict):
|
|
1717
|
+
result['npc_name'] = target_npc.name
|
|
1718
|
+
result['passed_from'] = self.name
|
|
1719
|
+
|
|
1720
|
+
return result
|
|
1721
|
+
|
|
658
1722
|
def to_dict(self):
|
|
659
1723
|
"""Convert NPC to dictionary representation"""
|
|
1724
|
+
jinx_rep = []
|
|
1725
|
+
if self.jinxs is not None:
|
|
1726
|
+
jinx_rep = [ jinx.to_dict() if isinstance(jinx, Jinx) else jinx for jinx in self.jinxs]
|
|
660
1727
|
return {
|
|
661
1728
|
"name": self.name,
|
|
662
1729
|
"primary_directive": self.primary_directive,
|
|
@@ -664,7 +1731,7 @@ class NPC:
|
|
|
664
1731
|
"provider": self.provider,
|
|
665
1732
|
"api_url": self.api_url,
|
|
666
1733
|
"api_key": self.api_key,
|
|
667
|
-
"jinxs":
|
|
1734
|
+
"jinxs": jinx_rep,
|
|
668
1735
|
"use_global_jinxs": self.use_global_jinxs
|
|
669
1736
|
}
|
|
670
1737
|
|
|
@@ -676,19 +1743,206 @@ class NPC:
|
|
|
676
1743
|
ensure_dirs_exist(directory)
|
|
677
1744
|
npc_path = os.path.join(directory, f"{self.name}.npc")
|
|
678
1745
|
|
|
679
|
-
return write_yaml_file(npc_path, self.to_dict())
|
|
680
|
-
|
|
681
|
-
def __str__(self):
|
|
682
|
-
"""String representation of NPC"""
|
|
683
|
-
|
|
1746
|
+
return write_yaml_file(npc_path, self.to_dict())
|
|
1747
|
+
|
|
1748
|
+
def __str__(self):
|
|
1749
|
+
"""String representation of NPC"""
|
|
1750
|
+
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.jinxs:
|
|
1752
|
+
str_rep += "Jinxs:\n"
|
|
1753
|
+
for jinx in self.jinxs:
|
|
1754
|
+
str_rep += f" - {jinx.jinx_name}\n"
|
|
1755
|
+
else:
|
|
1756
|
+
str_rep += "No jinxs available.\n"
|
|
1757
|
+
return str_rep
|
|
1758
|
+
|
|
1759
|
+
|
|
1760
|
+
|
|
1761
|
+
def execute_jinx_command(self,
|
|
1762
|
+
jinx: Jinx,
|
|
1763
|
+
args: List[str],
|
|
1764
|
+
messages=None,
|
|
1765
|
+
) -> Dict[str, Any]:
|
|
1766
|
+
"""
|
|
1767
|
+
Execute a jinx command with the given arguments.
|
|
1768
|
+
"""
|
|
1769
|
+
|
|
1770
|
+
input_values = extract_jinx_inputs(args, jinx)
|
|
1771
|
+
|
|
1772
|
+
|
|
1773
|
+
|
|
1774
|
+
|
|
1775
|
+
jinx_output = jinx.execute(
|
|
1776
|
+
input_values,
|
|
1777
|
+
jinx.jinx_name,
|
|
1778
|
+
npc=self,
|
|
1779
|
+
)
|
|
1780
|
+
|
|
1781
|
+
return {"messages": messages, "output": jinx_output}
|
|
1782
|
+
def create_memory(self, content: str, memory_type: str = "observation") -> Optional[int]:
|
|
1783
|
+
"""Create a new memory entry"""
|
|
1784
|
+
if not self.command_history:
|
|
1785
|
+
return None
|
|
1786
|
+
|
|
1787
|
+
message_id = generate_message_id()
|
|
1788
|
+
conversation_id = self.command_history.get_most_recent_conversation_id()
|
|
1789
|
+
conversation_id = conversation_id.get('conversation_id') if conversation_id else 'direct_memory'
|
|
1790
|
+
|
|
1791
|
+
team_name = getattr(self.team, 'name', 'default_team') if self.team else 'default_team'
|
|
1792
|
+
directory_path = os.getcwd()
|
|
1793
|
+
|
|
1794
|
+
return self.command_history.add_memory_to_database(
|
|
1795
|
+
message_id=message_id,
|
|
1796
|
+
conversation_id=conversation_id,
|
|
1797
|
+
npc=self.name,
|
|
1798
|
+
team=team_name,
|
|
1799
|
+
directory_path=directory_path,
|
|
1800
|
+
initial_memory=content,
|
|
1801
|
+
status='active',
|
|
1802
|
+
model=self.model,
|
|
1803
|
+
provider=self.provider
|
|
1804
|
+
)
|
|
1805
|
+
|
|
1806
|
+
def read_memory(self, memory_id: int) -> Optional[Dict[str, Any]]:
|
|
1807
|
+
"""Read a specific memory by ID"""
|
|
1808
|
+
if not self.command_history:
|
|
1809
|
+
return None
|
|
1810
|
+
|
|
1811
|
+
stmt = "SELECT * FROM memory_lifecycle WHERE id = :memory_id"
|
|
1812
|
+
return self.command_history._fetch_one(stmt, {"memory_id": memory_id})
|
|
1813
|
+
|
|
1814
|
+
def update_memory(self, memory_id: int, new_content: str = None, status: str = None) -> bool:
|
|
1815
|
+
"""Update memory content or status"""
|
|
1816
|
+
if not self.command_history:
|
|
1817
|
+
return False
|
|
1818
|
+
|
|
1819
|
+
updates = []
|
|
1820
|
+
params = {"memory_id": memory_id}
|
|
1821
|
+
|
|
1822
|
+
if new_content is not None:
|
|
1823
|
+
updates.append("final_memory = :final_memory")
|
|
1824
|
+
params["final_memory"] = new_content
|
|
1825
|
+
|
|
1826
|
+
if status is not None:
|
|
1827
|
+
updates.append("status = :status")
|
|
1828
|
+
params["status"] = status
|
|
1829
|
+
|
|
1830
|
+
if not updates:
|
|
1831
|
+
return False
|
|
1832
|
+
|
|
1833
|
+
stmt = f"UPDATE memory_lifecycle SET {', '.join(updates)} WHERE id = :memory_id"
|
|
1834
|
+
|
|
1835
|
+
try:
|
|
1836
|
+
with self.command_history.engine.begin() as conn:
|
|
1837
|
+
conn.execute(text(stmt), params)
|
|
1838
|
+
return True
|
|
1839
|
+
except Exception as e:
|
|
1840
|
+
print(f"Error updating memory {memory_id}: {e}")
|
|
1841
|
+
return False
|
|
1842
|
+
|
|
1843
|
+
def delete_memory(self, memory_id: int) -> bool:
|
|
1844
|
+
"""Delete a memory by ID"""
|
|
1845
|
+
if not self.command_history:
|
|
1846
|
+
return False
|
|
1847
|
+
|
|
1848
|
+
stmt = "DELETE FROM memory_lifecycle WHERE id = :memory_id AND npc = :npc"
|
|
1849
|
+
|
|
1850
|
+
try:
|
|
1851
|
+
with self.command_history.engine.begin() as conn:
|
|
1852
|
+
result = conn.execute(text(stmt), {"memory_id": memory_id, "npc": self.name})
|
|
1853
|
+
return result.rowcount > 0
|
|
1854
|
+
except Exception as e:
|
|
1855
|
+
print(f"Error deleting memory {memory_id}: {e}")
|
|
1856
|
+
return False
|
|
1857
|
+
|
|
1858
|
+
def search_memories(self, query: str, limit: int = 10, status_filter: str = None) -> List[Dict[str, Any]]:
|
|
1859
|
+
"""Search memories with optional status filtering"""
|
|
1860
|
+
if not self.command_history:
|
|
1861
|
+
return []
|
|
1862
|
+
|
|
1863
|
+
team_name = getattr(self.team, 'name', 'default_team') if self.team else 'default_team'
|
|
1864
|
+
directory_path = os.getcwd()
|
|
1865
|
+
|
|
1866
|
+
return self.command_history.search_memory(
|
|
1867
|
+
query=query,
|
|
1868
|
+
npc=self.name,
|
|
1869
|
+
team=team_name,
|
|
1870
|
+
directory_path=directory_path,
|
|
1871
|
+
status_filter=status_filter,
|
|
1872
|
+
limit=limit
|
|
1873
|
+
)
|
|
1874
|
+
|
|
1875
|
+
def get_all_memories(self, limit: int = 50, status_filter: str = None) -> List[Dict[str, Any]]:
|
|
1876
|
+
"""Get all memories for this NPC with optional status filtering"""
|
|
1877
|
+
if not self.command_history:
|
|
1878
|
+
return []
|
|
1879
|
+
|
|
1880
|
+
if limit is None:
|
|
1881
|
+
limit = 50
|
|
1882
|
+
|
|
1883
|
+
conditions = ["npc = :npc"]
|
|
1884
|
+
params = {"npc": self.name, "limit": limit}
|
|
1885
|
+
|
|
1886
|
+
if status_filter:
|
|
1887
|
+
conditions.append("status = :status")
|
|
1888
|
+
params["status"] = status_filter
|
|
1889
|
+
|
|
1890
|
+
stmt = f"""
|
|
1891
|
+
SELECT * FROM memory_lifecycle
|
|
1892
|
+
WHERE {' AND '.join(conditions)}
|
|
1893
|
+
ORDER BY created_at DESC
|
|
1894
|
+
LIMIT :limit
|
|
1895
|
+
"""
|
|
1896
|
+
|
|
1897
|
+
return self.command_history._fetch_all(stmt, params)
|
|
1898
|
+
|
|
1899
|
+
|
|
1900
|
+
def archive_old_memories(self, days_old: int = 30) -> int:
|
|
1901
|
+
"""Archive memories older than specified days"""
|
|
1902
|
+
if not self.command_history:
|
|
1903
|
+
return 0
|
|
1904
|
+
|
|
1905
|
+
stmt = """
|
|
1906
|
+
UPDATE memory_lifecycle
|
|
1907
|
+
SET status = 'archived'
|
|
1908
|
+
WHERE npc = :npc
|
|
1909
|
+
AND status = 'active'
|
|
1910
|
+
AND datetime(created_at) < datetime('now', '-{} days')
|
|
1911
|
+
""".format(days_old)
|
|
1912
|
+
|
|
1913
|
+
try:
|
|
1914
|
+
with self.command_history.engine.begin() as conn:
|
|
1915
|
+
result = conn.execute(text(stmt), {"npc": self.name})
|
|
1916
|
+
return result.rowcount
|
|
1917
|
+
except Exception as e:
|
|
1918
|
+
print(f"Error archiving memories: {e}")
|
|
1919
|
+
return 0
|
|
1920
|
+
|
|
1921
|
+
def get_memory_stats(self) -> Dict[str, int]:
|
|
1922
|
+
"""Get memory statistics for this NPC"""
|
|
1923
|
+
if not self.command_history:
|
|
1924
|
+
return {}
|
|
1925
|
+
|
|
1926
|
+
stmt = """
|
|
1927
|
+
SELECT status, COUNT(*) as count
|
|
1928
|
+
FROM memory_lifecycle
|
|
1929
|
+
WHERE npc = :npc
|
|
1930
|
+
GROUP BY status
|
|
1931
|
+
"""
|
|
1932
|
+
|
|
1933
|
+
results = self.command_history._fetch_all(stmt, {"npc": self.name})
|
|
1934
|
+
return {row['status']: row['count'] for row in results}
|
|
1935
|
+
|
|
684
1936
|
|
|
685
1937
|
class Team:
|
|
686
1938
|
def __init__(self,
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
1939
|
+
team_path=None,
|
|
1940
|
+
npcs=None,
|
|
1941
|
+
forenpc=None,
|
|
1942
|
+
jinxs=None,
|
|
1943
|
+
db_conn=None,
|
|
1944
|
+
model = None,
|
|
1945
|
+
provider = None):
|
|
692
1946
|
"""
|
|
693
1947
|
Initialize an NPC team from directory or list of NPCs
|
|
694
1948
|
|
|
@@ -697,97 +1951,157 @@ class Team:
|
|
|
697
1951
|
npcs: List of NPC objects
|
|
698
1952
|
db_conn: Database connection
|
|
699
1953
|
"""
|
|
1954
|
+
self.model = model
|
|
1955
|
+
self.provider = provider
|
|
1956
|
+
|
|
700
1957
|
self.npcs = {}
|
|
701
1958
|
self.sub_teams = {}
|
|
702
1959
|
self.jinxs_dict = jinxs or {}
|
|
703
1960
|
self.db_conn = db_conn
|
|
704
1961
|
self.team_path = os.path.expanduser(team_path) if team_path else None
|
|
705
|
-
self.databases =
|
|
706
|
-
self.mcp_servers =
|
|
1962
|
+
self.databases = []
|
|
1963
|
+
self.mcp_servers = []
|
|
707
1964
|
if forenpc is not None:
|
|
708
1965
|
self.forenpc = forenpc
|
|
709
1966
|
else:
|
|
710
1967
|
self.forenpc = npcs[0] if npcs else None
|
|
711
1968
|
|
|
712
|
-
|
|
713
1969
|
if team_path:
|
|
714
1970
|
self.name = os.path.basename(os.path.abspath(team_path))
|
|
715
1971
|
else:
|
|
716
1972
|
self.name = "custom_team"
|
|
717
|
-
self.context =
|
|
1973
|
+
self.context = ''
|
|
718
1974
|
self.shared_context = {
|
|
719
1975
|
"intermediate_results": {},
|
|
720
1976
|
"dataframes": {},
|
|
721
1977
|
"memories": {},
|
|
722
1978
|
"execution_history": [],
|
|
723
|
-
"npc_messages": {}
|
|
1979
|
+
"npc_messages": {},
|
|
1980
|
+
"context":''
|
|
724
1981
|
}
|
|
725
1982
|
|
|
726
1983
|
if team_path:
|
|
727
|
-
print('loading npc team from directory')
|
|
728
1984
|
self._load_from_directory()
|
|
729
1985
|
|
|
730
1986
|
elif npcs:
|
|
731
1987
|
for npc in npcs:
|
|
732
1988
|
self.npcs[npc.name] = npc
|
|
733
|
-
|
|
734
1989
|
|
|
735
|
-
|
|
736
1990
|
self.jinja_env = Environment(undefined=SilentUndefined)
|
|
737
1991
|
|
|
738
|
-
|
|
739
1992
|
if db_conn is not None:
|
|
740
1993
|
init_db_tables()
|
|
1994
|
+
|
|
1995
|
+
def update_context(self, messages: list):
|
|
1996
|
+
"""Update team context based on recent conversation patterns"""
|
|
1997
|
+
if len(messages) < 10:
|
|
1998
|
+
return
|
|
741
1999
|
|
|
2000
|
+
summary = breathe(
|
|
2001
|
+
messages=messages[-10:],
|
|
2002
|
+
npc=self.forenpc
|
|
2003
|
+
)
|
|
2004
|
+
characterization = summary.get('output')
|
|
742
2005
|
|
|
2006
|
+
if characterization:
|
|
2007
|
+
team_ctx_path = os.path.join(self.team_path, "team.ctx")
|
|
2008
|
+
|
|
2009
|
+
if os.path.exists(team_ctx_path):
|
|
2010
|
+
with open(team_ctx_path, 'r') as f:
|
|
2011
|
+
ctx_data = yaml.safe_load(f) or {}
|
|
2012
|
+
else:
|
|
2013
|
+
ctx_data = {}
|
|
2014
|
+
|
|
2015
|
+
current_context = ctx_data.get('context', '')
|
|
2016
|
+
|
|
2017
|
+
prompt = f"""Based on this characterization: {characterization},
|
|
2018
|
+
suggest changes to the team's context.
|
|
2019
|
+
Current Context: "{current_context}".
|
|
2020
|
+
Respond with JSON: {{"suggestion": "Your sentence."}}"""
|
|
2021
|
+
|
|
2022
|
+
response = get_llm_response(
|
|
2023
|
+
prompt=prompt,
|
|
2024
|
+
npc=self.forenpc,
|
|
2025
|
+
format="json"
|
|
2026
|
+
)
|
|
2027
|
+
suggestion = response.get("response", {}).get("suggestion")
|
|
2028
|
+
|
|
2029
|
+
if suggestion:
|
|
2030
|
+
new_context = (current_context + " " + suggestion).strip()
|
|
2031
|
+
user_approval = input(f"Update context to: {new_context}? [y/N]: ").strip().lower()
|
|
2032
|
+
if user_approval == 'y':
|
|
2033
|
+
ctx_data['context'] = new_context
|
|
2034
|
+
self.context = new_context
|
|
2035
|
+
with open(team_ctx_path, 'w') as f:
|
|
2036
|
+
yaml.dump(ctx_data, f)
|
|
2037
|
+
|
|
743
2038
|
def _load_from_directory(self):
|
|
744
2039
|
"""Load team from directory"""
|
|
745
2040
|
if not os.path.exists(self.team_path):
|
|
746
2041
|
raise ValueError(f"Team directory not found: {self.team_path}")
|
|
747
2042
|
|
|
748
|
-
# Load team context if available
|
|
749
|
-
self.context = self._load_team_context()
|
|
750
|
-
# Load NPCs
|
|
751
2043
|
for filename in os.listdir(self.team_path):
|
|
752
|
-
print('filename: ', filename)
|
|
753
2044
|
if filename.endswith(".npc"):
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
self.npcs[npc.name] = 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
|
|
758
2048
|
|
|
759
|
-
|
|
760
|
-
print(f"Error loading NPC {filename}: {e}")
|
|
2049
|
+
self.context = self._load_team_context()
|
|
761
2050
|
|
|
762
|
-
# Load jinxs from jinxs directory
|
|
763
2051
|
jinxs_dir = os.path.join(self.team_path, "jinxs")
|
|
764
2052
|
if os.path.exists(jinxs_dir):
|
|
765
2053
|
for jinx in load_jinxs_from_directory(jinxs_dir):
|
|
766
2054
|
self.jinxs_dict[jinx.jinx_name] = jinx
|
|
767
2055
|
|
|
768
|
-
# Load sub-teams (subfolders)
|
|
769
2056
|
self._load_sub_teams()
|
|
770
|
-
|
|
2057
|
+
|
|
771
2058
|
def _load_team_context(self):
|
|
772
2059
|
"""Load team context from .ctx file"""
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
#check if any .ctx file exists
|
|
776
2060
|
for fname in os.listdir(self.team_path):
|
|
777
2061
|
if fname.endswith('.ctx'):
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
ctx_data = load_yaml_file(os.path.join(self.team_path, fname))
|
|
781
|
-
|
|
2062
|
+
ctx_data = load_yaml_file(os.path.join(self.team_path, fname))
|
|
782
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
|
+
|
|
783
2081
|
if 'mcp_servers' in ctx_data:
|
|
784
2082
|
self.mcp_servers = ctx_data['mcp_servers']
|
|
2083
|
+
else:
|
|
2084
|
+
self.mcp_servers = []
|
|
785
2085
|
if 'databases' in ctx_data:
|
|
786
2086
|
self.databases = ctx_data['databases']
|
|
787
|
-
|
|
788
|
-
|
|
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
|
|
789
2103
|
for key, item in ctx_data.items():
|
|
790
|
-
if key not in ['name', 'mcp_servers', 'databases', 'context']:
|
|
2104
|
+
if key not in ['name', 'mcp_servers', 'databases', 'context', 'file_patterns']:
|
|
791
2105
|
self.shared_context[key] = item
|
|
792
2106
|
return ctx_data
|
|
793
2107
|
return {}
|
|
@@ -800,14 +2114,10 @@ class Team:
|
|
|
800
2114
|
not item.startswith('.') and
|
|
801
2115
|
item != "jinxs"):
|
|
802
2116
|
|
|
803
|
-
# Check if directory contains NPCs
|
|
804
2117
|
if any(f.endswith(".npc") for f in os.listdir(item_path)
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
self.sub_teams[item] = sub_team
|
|
809
|
-
except Exception as e:
|
|
810
|
-
print(f"Error loading sub-team {item}: {e}")
|
|
2118
|
+
if os.path.isfile(os.path.join(item_path, f))):
|
|
2119
|
+
sub_team = Team(team_path=item_path, db_conn=self.db_conn)
|
|
2120
|
+
self.sub_teams[item] = sub_team
|
|
811
2121
|
|
|
812
2122
|
def get_forenpc(self):
|
|
813
2123
|
"""
|
|
@@ -820,9 +2130,7 @@ class Team:
|
|
|
820
2130
|
if hasattr(self, 'context') and self.context and 'forenpc' in self.context:
|
|
821
2131
|
forenpc_ref = self.context['forenpc']
|
|
822
2132
|
|
|
823
|
-
# Handle Jinja template references
|
|
824
2133
|
if '{{ref(' in forenpc_ref:
|
|
825
|
-
# Extract NPC name from {{ref('npc_name')}}
|
|
826
2134
|
match = re.search(r"{{\s*ref\('([^']+)'\)\s*}}", forenpc_ref)
|
|
827
2135
|
if match:
|
|
828
2136
|
forenpc_name = match.group(1)
|
|
@@ -837,7 +2145,7 @@ class Team:
|
|
|
837
2145
|
forenpc_api_url=self.context.get('api_url', None)
|
|
838
2146
|
|
|
839
2147
|
forenpc = NPC(name='forenpc',
|
|
840
|
-
|
|
2148
|
+
primary_directive="""You are the forenpc of the team, coordinating activities
|
|
841
2149
|
between NPCs on the team, verifying that results from
|
|
842
2150
|
NPCs are high quality and can help to adequately answer
|
|
843
2151
|
user requests.""",
|
|
@@ -846,52 +2154,53 @@ class Team:
|
|
|
846
2154
|
api_key=forenpc_api_key,
|
|
847
2155
|
api_url=forenpc_api_url,
|
|
848
2156
|
)
|
|
2157
|
+
self.forenpc = forenpc
|
|
2158
|
+
self.npcs[forenpc.name] = forenpc
|
|
2159
|
+
return forenpc
|
|
849
2160
|
return None
|
|
850
|
-
|
|
2161
|
+
|
|
851
2162
|
def get_npc(self, npc_ref):
|
|
852
|
-
"""
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
2163
|
+
"""Get NPC by name or reference with hierarchical lookup capability"""
|
|
2164
|
+
if isinstance(npc_ref, NPC):
|
|
2165
|
+
return npc_ref
|
|
2166
|
+
elif isinstance(npc_ref, str):
|
|
2167
|
+
if npc_ref in self.npcs:
|
|
2168
|
+
return self.npcs[npc_ref]
|
|
2169
|
+
|
|
2170
|
+
for sub_team_name, sub_team in self.sub_teams.items():
|
|
2171
|
+
if npc_ref in sub_team.npcs:
|
|
2172
|
+
return sub_team.npcs[npc_ref]
|
|
2173
|
+
|
|
2174
|
+
result = sub_team.get_npc(npc_ref)
|
|
2175
|
+
if result:
|
|
2176
|
+
return result
|
|
2177
|
+
|
|
2178
|
+
return None
|
|
2179
|
+
else:
|
|
2180
|
+
return None
|
|
2181
|
+
|
|
867
2182
|
def orchestrate(self, request):
|
|
868
2183
|
"""Orchestrate a request through the team"""
|
|
869
2184
|
forenpc = self.get_forenpc()
|
|
870
2185
|
if not forenpc:
|
|
871
2186
|
return {"error": "No forenpc available to coordinate the team"}
|
|
872
2187
|
|
|
873
|
-
# Log the orchestration start
|
|
874
2188
|
log_entry(
|
|
875
2189
|
self.name,
|
|
876
2190
|
"orchestration_start",
|
|
877
2191
|
{"request": request}
|
|
878
2192
|
)
|
|
879
2193
|
|
|
880
|
-
# Initial request goes to forenpc
|
|
881
2194
|
result = forenpc.check_llm_command(request,
|
|
882
2195
|
context=getattr(self, 'context', {}),
|
|
883
|
-
#shared_context=self.shared_context,
|
|
884
2196
|
team = self,
|
|
885
2197
|
)
|
|
886
2198
|
|
|
887
|
-
# Track execution until complete
|
|
888
2199
|
while True:
|
|
889
|
-
# Save the result
|
|
890
2200
|
completion_prompt= ""
|
|
891
2201
|
if isinstance(result, dict):
|
|
892
2202
|
self.shared_context["execution_history"].append(result)
|
|
893
2203
|
|
|
894
|
-
# Track messages by NPC
|
|
895
2204
|
if result.get("messages") and result.get("npc_name"):
|
|
896
2205
|
if result["npc_name"] not in self.shared_context["npc_messages"]:
|
|
897
2206
|
self.shared_context["npc_messages"][result["npc_name"]] = []
|
|
@@ -936,7 +2245,7 @@ class Team:
|
|
|
936
2245
|
of misunderstanding, but as long as the response is clearly relevant
|
|
937
2246
|
to the input request and along the user's intended direction,
|
|
938
2247
|
it is considered relevant.
|
|
939
|
-
|
|
2248
|
+
|
|
940
2249
|
|
|
941
2250
|
If there is enough information to begin a fruitful conversation with the user,
|
|
942
2251
|
please consider the request relevant so that we do not
|
|
@@ -951,7 +2260,7 @@ class Team:
|
|
|
951
2260
|
-'relevant' with boolean value
|
|
952
2261
|
-'explanation' for irrelevance with quoted citations in your explanation noting why it is irrelevant to user input must be a single string.
|
|
953
2262
|
Return only the JSON object."""
|
|
954
|
-
|
|
2263
|
+
|
|
955
2264
|
completion_check = npy.llm_funcs.get_llm_response(
|
|
956
2265
|
completion_prompt,
|
|
957
2266
|
model=forenpc.model,
|
|
@@ -961,19 +2270,15 @@ class Team:
|
|
|
961
2270
|
npc=forenpc,
|
|
962
2271
|
format="json"
|
|
963
2272
|
)
|
|
964
|
-
|
|
2273
|
+
|
|
965
2274
|
if isinstance(completion_check.get("response"), dict):
|
|
966
2275
|
complete = completion_check["response"].get("relevant", False)
|
|
967
2276
|
explanation = completion_check["response"].get("explanation", "")
|
|
968
2277
|
else:
|
|
969
|
-
# Default to incomplete if format is wrong
|
|
970
2278
|
complete = False
|
|
971
2279
|
explanation = "Could not determine completion status"
|
|
972
2280
|
|
|
973
|
-
#import pdb
|
|
974
|
-
#pdb.set_trace()
|
|
975
2281
|
if complete:
|
|
976
|
-
|
|
977
2282
|
debrief = npy.llm_funcs.get_llm_response(
|
|
978
2283
|
f"""Context:
|
|
979
2284
|
Original request: {request}
|
|
@@ -993,14 +2298,12 @@ class Team:
|
|
|
993
2298
|
format="json"
|
|
994
2299
|
)
|
|
995
2300
|
|
|
996
|
-
|
|
997
2301
|
return {
|
|
998
2302
|
"debrief": debrief.get("response"),
|
|
999
2303
|
"output": result.get("output"),
|
|
1000
2304
|
"execution_history": self.shared_context["execution_history"],
|
|
1001
2305
|
}
|
|
1002
2306
|
else:
|
|
1003
|
-
# Continue with updated request
|
|
1004
2307
|
updated_request = (
|
|
1005
2308
|
request
|
|
1006
2309
|
+ "\n\nThe request has not yet been fully completed. "
|
|
@@ -1008,9 +2311,7 @@ class Team:
|
|
|
1008
2311
|
+ "\nPlease address only the remaining parts of the request."
|
|
1009
2312
|
)
|
|
1010
2313
|
print('updating request', updated_request)
|
|
1011
|
-
|
|
1012
2314
|
|
|
1013
|
-
# Call forenpc again
|
|
1014
2315
|
result = forenpc.check_llm_command(
|
|
1015
2316
|
updated_request,
|
|
1016
2317
|
context=getattr(self, 'context', {}),
|
|
@@ -1037,531 +2338,101 @@ class Team:
|
|
|
1037
2338
|
if not directory:
|
|
1038
2339
|
raise ValueError("No directory specified for saving team")
|
|
1039
2340
|
|
|
1040
|
-
# Create team directory
|
|
1041
2341
|
ensure_dirs_exist(directory)
|
|
1042
2342
|
|
|
1043
|
-
# Save context
|
|
1044
2343
|
if hasattr(self, 'context') and self.context:
|
|
1045
2344
|
ctx_path = os.path.join(directory, "team.ctx")
|
|
1046
2345
|
write_yaml_file(ctx_path, self.context)
|
|
1047
2346
|
|
|
1048
|
-
# Save NPCs
|
|
1049
2347
|
for npc in self.npcs.values():
|
|
1050
2348
|
npc.save(directory)
|
|
1051
2349
|
|
|
1052
|
-
# Create jinxs directory
|
|
1053
2350
|
jinxs_dir = os.path.join(directory, "jinxs")
|
|
1054
2351
|
ensure_dirs_exist(jinxs_dir)
|
|
1055
2352
|
|
|
1056
|
-
# Save jinxs
|
|
1057
2353
|
for jinx in self.jinxs.values():
|
|
1058
2354
|
jinx.save(jinxs_dir)
|
|
1059
2355
|
|
|
1060
|
-
# Save sub-teams
|
|
1061
2356
|
for team_name, team in self.sub_teams.items():
|
|
1062
2357
|
team_dir = os.path.join(directory, team_name)
|
|
1063
2358
|
team.save(team_dir)
|
|
1064
2359
|
|
|
1065
2360
|
return True
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
class Pipeline:
|
|
1072
|
-
def __init__(self, pipeline_data=None, pipeline_path=None, npc_team=None):
|
|
1073
|
-
"""Initialize a pipeline from data or file path"""
|
|
1074
|
-
self.npc_team = npc_team
|
|
1075
|
-
self.steps = []
|
|
1076
|
-
|
|
1077
|
-
if pipeline_path:
|
|
1078
|
-
self._load_from_path(pipeline_path)
|
|
1079
|
-
elif pipeline_data:
|
|
1080
|
-
self.name = pipeline_data.get("name", "unnamed_pipeline")
|
|
1081
|
-
self.steps = pipeline_data.get("steps", [])
|
|
1082
|
-
else:
|
|
1083
|
-
raise ValueError("Either pipeline_data or pipeline_path must be provided")
|
|
1084
|
-
|
|
1085
|
-
def _load_from_path(self, path):
|
|
1086
|
-
"""Load pipeline from file"""
|
|
1087
|
-
pipeline_data = load_yaml_file(path)
|
|
1088
|
-
if not pipeline_data:
|
|
1089
|
-
raise ValueError(f"Failed to load pipeline from {path}")
|
|
1090
|
-
|
|
1091
|
-
self.name = os.path.splitext(os.path.basename(path))[0]
|
|
1092
|
-
self.steps = pipeline_data.get("steps", [])
|
|
1093
|
-
self.pipeline_path = path
|
|
1094
|
-
|
|
1095
|
-
def execute(self, initial_context=None):
|
|
1096
|
-
"""Execute the pipeline with given context"""
|
|
1097
|
-
context = initial_context or {}
|
|
1098
|
-
results = {}
|
|
1099
|
-
|
|
1100
|
-
# Initialize database tables
|
|
1101
|
-
init_db_tables()
|
|
2361
|
+
def _parse_file_patterns(self, patterns_config):
|
|
2362
|
+
"""Parse file patterns configuration and load matching files into KV cache"""
|
|
2363
|
+
if not patterns_config:
|
|
2364
|
+
return {}
|
|
1102
2365
|
|
|
1103
|
-
|
|
1104
|
-
pipeline_hash = self._generate_hash()
|
|
1105
|
-
|
|
1106
|
-
# Create results table specific to this pipeline
|
|
1107
|
-
results_table = f"{self.name}_results"
|
|
1108
|
-
self._ensure_results_table(results_table)
|
|
1109
|
-
|
|
1110
|
-
# Create run entry
|
|
1111
|
-
run_id = self._create_run_entry(pipeline_hash)
|
|
1112
|
-
|
|
1113
|
-
# Add utility functions to context
|
|
1114
|
-
context.update({
|
|
1115
|
-
"ref": lambda step_name: results.get(step_name),
|
|
1116
|
-
"source": self._fetch_data_from_source,
|
|
1117
|
-
})
|
|
2366
|
+
file_cache = {}
|
|
1118
2367
|
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
if not step_name:
|
|
1123
|
-
raise ValueError(f"Missing step_name in step: {step}")
|
|
1124
|
-
|
|
1125
|
-
# Get NPC for this step
|
|
1126
|
-
npc_name = self._render_template(step.get("npc", ""), context)
|
|
1127
|
-
npc = self._get_npc(npc_name)
|
|
1128
|
-
if not npc:
|
|
1129
|
-
raise ValueError(f"NPC {npc_name} not found for step {step_name}")
|
|
1130
|
-
|
|
1131
|
-
# Render task template
|
|
1132
|
-
task = self._render_template(step.get("task", ""), context)
|
|
1133
|
-
|
|
1134
|
-
# Execute with appropriate NPC
|
|
1135
|
-
model = step.get("model", npc.model)
|
|
1136
|
-
provider = step.get("provider", npc.provider)
|
|
2368
|
+
for pattern_entry in patterns_config:
|
|
2369
|
+
if isinstance(pattern_entry, str):
|
|
2370
|
+
pattern_entry = {"pattern": pattern_entry}
|
|
1137
2371
|
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
response = self._execute_mixa_step(step, context, npc, model, provider)
|
|
1142
|
-
else:
|
|
1143
|
-
# Check for data source
|
|
1144
|
-
source_matches = re.findall(r"{{\s*source\('([^']+)'\)\s*}}", task)
|
|
1145
|
-
if source_matches:
|
|
1146
|
-
response = self._execute_data_source_step(step, context, source_matches, npc, model, provider)
|
|
1147
|
-
else:
|
|
1148
|
-
# Standard LLM execution
|
|
1149
|
-
llm_response = npy.llm_funcs.get_llm_response(task, model=model, provider=provider, npc=npc)
|
|
1150
|
-
response = llm_response.get("response", "")
|
|
2372
|
+
pattern = pattern_entry.get("pattern", "")
|
|
2373
|
+
recursive = pattern_entry.get("recursive", False)
|
|
2374
|
+
base_path = pattern_entry.get("base_path", ".")
|
|
1151
2375
|
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
2376
|
+
if not pattern:
|
|
2377
|
+
continue
|
|
2378
|
+
|
|
2379
|
+
base_path = os.path.expanduser(base_path)
|
|
2380
|
+
if not os.path.isabs(base_path):
|
|
2381
|
+
base_path = os.path.join(self.team_path or os.getcwd(), base_path)
|
|
1155
2382
|
|
|
1156
|
-
|
|
1157
|
-
self._store_step_result(run_id, step_name, npc_name, model, provider,
|
|
1158
|
-
{"task": task}, response, results_table)
|
|
2383
|
+
matching_files = self._find_matching_files(pattern, base_path, recursive)
|
|
1159
2384
|
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
2385
|
+
for file_path in matching_files:
|
|
2386
|
+
file_content = self._load_file_content(file_path)
|
|
2387
|
+
if file_content:
|
|
2388
|
+
relative_path = os.path.relpath(file_path, base_path)
|
|
2389
|
+
file_cache[relative_path] = file_content
|
|
1165
2390
|
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
try:
|
|
1172
|
-
template = Template(template_str)
|
|
1173
|
-
return template.render(**context)
|
|
1174
|
-
except Exception as e:
|
|
1175
|
-
print(f"Error rendering template: {e}")
|
|
1176
|
-
return template_str
|
|
1177
|
-
|
|
1178
|
-
def _get_npc(self, npc_name):
|
|
1179
|
-
"""Get NPC by name from team"""
|
|
1180
|
-
if not self.npc_team:
|
|
1181
|
-
raise ValueError("No NPC team available")
|
|
1182
|
-
|
|
1183
|
-
return self.npc_team.get_npc(npc_name)
|
|
1184
|
-
|
|
1185
|
-
def _generate_hash(self):
|
|
1186
|
-
"""Generate a hash for the pipeline"""
|
|
1187
|
-
if hasattr(self, 'pipeline_path') and self.pipeline_path:
|
|
1188
|
-
with open(self.pipeline_path, 'r') as f:
|
|
1189
|
-
content = f.read()
|
|
1190
|
-
return hashlib.sha256(content.encode()).hexdigest()
|
|
1191
|
-
else:
|
|
1192
|
-
# Generate hash from steps
|
|
1193
|
-
content = json.dumps(self.steps)
|
|
1194
|
-
return hashlib.sha256(content.encode()).hexdigest()
|
|
1195
|
-
|
|
1196
|
-
def _ensure_results_table(self, table_name):
|
|
1197
|
-
"""Ensure results table exists"""
|
|
1198
|
-
db_path = "~/npcsh_history.db"
|
|
1199
|
-
with sqlite3.connect(os.path.expanduser(db_path)) as conn:
|
|
1200
|
-
conn.execute(f"""
|
|
1201
|
-
CREATE TABLE IF NOT EXISTS {table_name} (
|
|
1202
|
-
result_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1203
|
-
run_id INTEGER,
|
|
1204
|
-
step_name TEXT,
|
|
1205
|
-
npc_name TEXT,
|
|
1206
|
-
model TEXT,
|
|
1207
|
-
provider TEXT,
|
|
1208
|
-
inputs TEXT,
|
|
1209
|
-
outputs TEXT,
|
|
1210
|
-
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
1211
|
-
FOREIGN KEY(run_id) REFERENCES pipeline_runs(run_id)
|
|
1212
|
-
)
|
|
1213
|
-
""")
|
|
1214
|
-
conn.commit()
|
|
1215
|
-
|
|
1216
|
-
def _create_run_entry(self, pipeline_hash):
|
|
1217
|
-
"""Create run entry in pipeline_runs table"""
|
|
1218
|
-
db_path = "~/npcsh_history.db"
|
|
1219
|
-
with sqlite3.connect(os.path.expanduser(db_path)) as conn:
|
|
1220
|
-
cursor = conn.execute(
|
|
1221
|
-
"INSERT INTO pipeline_runs (pipeline_name, pipeline_hash, timestamp) VALUES (?, ?, ?)",
|
|
1222
|
-
(self.name, pipeline_hash, datetime.now())
|
|
1223
|
-
)
|
|
1224
|
-
conn.commit()
|
|
1225
|
-
return cursor.lastrowid
|
|
1226
|
-
|
|
1227
|
-
def _store_step_result(self, run_id, step_name, npc_name, model, provider, inputs, outputs, table_name):
|
|
1228
|
-
"""Store step result in database"""
|
|
1229
|
-
db_path = "~/npcsh_history.db"
|
|
1230
|
-
with sqlite3.connect(os.path.expanduser(db_path)) as conn:
|
|
1231
|
-
conn.execute(
|
|
1232
|
-
f"""
|
|
1233
|
-
INSERT INTO {table_name}
|
|
1234
|
-
(run_id, step_name, npc_name, model, provider, inputs, outputs)
|
|
1235
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1236
|
-
""",
|
|
1237
|
-
(
|
|
1238
|
-
run_id,
|
|
1239
|
-
step_name,
|
|
1240
|
-
npc_name,
|
|
1241
|
-
model,
|
|
1242
|
-
provider,
|
|
1243
|
-
json.dumps(self._clean_for_json(inputs)),
|
|
1244
|
-
json.dumps(self._clean_for_json(outputs))
|
|
1245
|
-
)
|
|
1246
|
-
)
|
|
1247
|
-
conn.commit()
|
|
1248
|
-
|
|
1249
|
-
def _clean_for_json(self, obj):
|
|
1250
|
-
"""Clean an object for JSON serialization"""
|
|
1251
|
-
if isinstance(obj, dict):
|
|
1252
|
-
return {
|
|
1253
|
-
k: self._clean_for_json(v)
|
|
1254
|
-
for k, v in obj.items()
|
|
1255
|
-
if not k.startswith("_") and not callable(v)
|
|
1256
|
-
}
|
|
1257
|
-
elif isinstance(obj, list):
|
|
1258
|
-
return [self._clean_for_json(i) for i in obj]
|
|
1259
|
-
elif isinstance(obj, (str, int, float, bool, type(None))):
|
|
1260
|
-
return obj
|
|
1261
|
-
else:
|
|
1262
|
-
return str(obj)
|
|
1263
|
-
|
|
1264
|
-
def _fetch_data_from_source(self, table_name):
|
|
1265
|
-
"""Fetch data from a database table"""
|
|
1266
|
-
db_path = "~/npcsh_history.db"
|
|
1267
|
-
try:
|
|
1268
|
-
engine = create_engine(f"sqlite:///{os.path.expanduser(db_path)}")
|
|
1269
|
-
df = pd.read_sql(f"SELECT * FROM {table_name}", engine)
|
|
1270
|
-
return df.to_json(orient="records")
|
|
1271
|
-
except Exception as e:
|
|
1272
|
-
print(f"Error fetching data from {table_name}: {e}")
|
|
1273
|
-
return "[]"
|
|
1274
|
-
|
|
1275
|
-
def _execute_mixa_step(self, step, context, npc, model, provider):
|
|
1276
|
-
"""Execute a mixture of agents step"""
|
|
1277
|
-
# Get task template
|
|
1278
|
-
task = self._render_template(step.get("task", ""), context)
|
|
1279
|
-
|
|
1280
|
-
# Get configuration
|
|
1281
|
-
mixa_turns = step.get("mixa_turns", 5)
|
|
1282
|
-
num_generating_agents = len(step.get("mixa_agents", []))
|
|
1283
|
-
if num_generating_agents == 0:
|
|
1284
|
-
num_generating_agents = 3 # Default
|
|
1285
|
-
|
|
1286
|
-
num_voting_agents = len(step.get("mixa_voters", []))
|
|
1287
|
-
if num_voting_agents == 0:
|
|
1288
|
-
num_voting_agents = 3 # Default
|
|
1289
|
-
|
|
1290
|
-
# Step 1: Initial Response Generation
|
|
1291
|
-
round_responses = []
|
|
1292
|
-
for _ in range(num_generating_agents):
|
|
1293
|
-
response = npy.llm_funcs.get_llm_response(task, model=model, provider=provider, npc=npc)
|
|
1294
|
-
round_responses.append(response.get("response", ""))
|
|
1295
|
-
|
|
1296
|
-
# Loop for each round of voting and refining
|
|
1297
|
-
for turn in range(1, mixa_turns + 1):
|
|
1298
|
-
# Step 2: Voting by agents
|
|
1299
|
-
votes = [0] * len(round_responses)
|
|
1300
|
-
for _ in range(num_voting_agents):
|
|
1301
|
-
voted_index = random.choice(range(len(round_responses)))
|
|
1302
|
-
votes[voted_index] += 1
|
|
1303
|
-
|
|
1304
|
-
# Step 3: Refinement feedback
|
|
1305
|
-
refined_responses = []
|
|
1306
|
-
for i, resp in enumerate(round_responses):
|
|
1307
|
-
feedback = (
|
|
1308
|
-
f"Current responses and their votes:\n" +
|
|
1309
|
-
"\n".join([f"Response {j+1}: {r[:100]}... - Votes: {votes[j]}"
|
|
1310
|
-
for j, r in enumerate(round_responses)]) +
|
|
1311
|
-
f"\n\nRefine your response #{i+1}: {resp}"
|
|
1312
|
-
)
|
|
1313
|
-
|
|
1314
|
-
response = npy.llm_funcs.get_llm_response(feedback, model=model, provider=provider, npc=npc)
|
|
1315
|
-
refined_responses.append(response.get("response", ""))
|
|
1316
|
-
|
|
1317
|
-
# Update responses for next round
|
|
1318
|
-
round_responses = refined_responses
|
|
1319
|
-
|
|
1320
|
-
# Final synthesis
|
|
1321
|
-
synthesis_prompt = (
|
|
1322
|
-
"Synthesize these responses into a coherent answer:\n" +
|
|
1323
|
-
"\n".join(round_responses)
|
|
1324
|
-
)
|
|
1325
|
-
final_response = npy.llm_funcs.get_llm_response(synthesis_prompt, model=model, provider=provider, npc=npc)
|
|
2391
|
+
return file_cache
|
|
2392
|
+
|
|
2393
|
+
def _find_matching_files(self, pattern, base_path, recursive=False):
|
|
2394
|
+
"""Find files matching the given pattern"""
|
|
2395
|
+
matching_files = []
|
|
1326
2396
|
|
|
1327
|
-
|
|
2397
|
+
if not os.path.exists(base_path):
|
|
2398
|
+
return matching_files
|
|
1328
2399
|
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
2400
|
+
if recursive:
|
|
2401
|
+
for root, dirs, files in os.walk(base_path):
|
|
2402
|
+
for filename in files:
|
|
2403
|
+
if fnmatch.fnmatch(filename, pattern):
|
|
2404
|
+
matching_files.append(os.path.join(root, filename))
|
|
2405
|
+
else:
|
|
2406
|
+
try:
|
|
2407
|
+
for item in os.listdir(base_path):
|
|
2408
|
+
item_path = os.path.join(base_path, item)
|
|
2409
|
+
if os.path.isfile(item_path) and fnmatch.fnmatch(item, pattern):
|
|
2410
|
+
matching_files.append(item_path)
|
|
2411
|
+
except PermissionError:
|
|
2412
|
+
print(f"Permission denied accessing {base_path}")
|
|
1333
2413
|
|
|
2414
|
+
return matching_files
|
|
2415
|
+
|
|
2416
|
+
def _load_file_content(self, file_path):
|
|
2417
|
+
"""Load content from a file with error handling"""
|
|
1334
2418
|
try:
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
engine = create_engine(f"sqlite:///{os.path.expanduser(db_path)}")
|
|
1338
|
-
df = pd.read_sql(f"SELECT * FROM {table_name}", engine)
|
|
1339
|
-
|
|
1340
|
-
# Handle batch mode vs. individual processing
|
|
1341
|
-
if step.get("batch_mode", False):
|
|
1342
|
-
# Replace source reference with all data
|
|
1343
|
-
data_str = df.to_json(orient="records")
|
|
1344
|
-
task = task_template.replace(f"{{{{ source('{table_name}') }}}}", data_str)
|
|
1345
|
-
task = self._render_template(task, context)
|
|
1346
|
-
|
|
1347
|
-
# Process all at once
|
|
1348
|
-
response = npy.llm_funcs.get_llm_response(task, model=model, provider=provider, npc=npc)
|
|
1349
|
-
return response.get("response", "")
|
|
1350
|
-
else:
|
|
1351
|
-
# Process each row individually
|
|
1352
|
-
results = []
|
|
1353
|
-
for _, row in df.iterrows():
|
|
1354
|
-
# Replace source reference with row data
|
|
1355
|
-
row_data = json.dumps(row.to_dict())
|
|
1356
|
-
row_task = task_template.replace(f"{{{{ source('{table_name}') }}}}", row_data)
|
|
1357
|
-
row_task = self._render_template(row_task, context)
|
|
1358
|
-
|
|
1359
|
-
# Process row
|
|
1360
|
-
response = npy.llm_funcs.get_llm_response(row_task, model=model, provider=provider, npc=npc)
|
|
1361
|
-
results.append(response.get("response", ""))
|
|
1362
|
-
|
|
1363
|
-
return results
|
|
2419
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
2420
|
+
return f.read()
|
|
1364
2421
|
except Exception as e:
|
|
1365
|
-
print(f"Error
|
|
1366
|
-
return
|
|
1367
|
-
|
|
2422
|
+
print(f"Error reading {file_path}: {e}")
|
|
2423
|
+
return None
|
|
1368
2424
|
|
|
1369
2425
|
|
|
1370
|
-
def
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
conn.execute(
|
|
1375
|
-
"INSERT INTO npc_log (entity_id, entry_type, content, metadata) VALUES (?, ?, ?, ?)",
|
|
1376
|
-
(entity_id, entry_type, json.dumps(content), json.dumps(metadata) if metadata else None)
|
|
1377
|
-
)
|
|
1378
|
-
conn.commit()
|
|
1379
|
-
|
|
1380
|
-
def get_log_entries(entity_id, entry_type=None, limit=10, db_path="~/npcsh_history.db"):
|
|
1381
|
-
"""Get log entries for an NPC or team"""
|
|
1382
|
-
db_path = os.path.expanduser(db_path)
|
|
1383
|
-
with sqlite3.connect(db_path) as conn:
|
|
1384
|
-
query = "SELECT entry_type, content, metadata, timestamp FROM npc_log WHERE entity_id = ?"
|
|
1385
|
-
params = [entity_id]
|
|
1386
|
-
|
|
1387
|
-
if entry_type:
|
|
1388
|
-
query += " AND entry_type = ?"
|
|
1389
|
-
params.append(entry_type)
|
|
1390
|
-
|
|
1391
|
-
query += " ORDER BY timestamp DESC LIMIT ?"
|
|
1392
|
-
params.append(limit)
|
|
2426
|
+
def _format_parsed_files_context(self, parsed_files):
|
|
2427
|
+
"""Format parsed files into context string"""
|
|
2428
|
+
if not parsed_files:
|
|
2429
|
+
return ""
|
|
1393
2430
|
|
|
1394
|
-
|
|
2431
|
+
context_parts = ["Additional context from files:"]
|
|
1395
2432
|
|
|
1396
|
-
|
|
1397
|
-
{
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
"metadata": json.loads(r[2]) if r[2] else None,
|
|
1401
|
-
"timestamp": r[3]
|
|
1402
|
-
}
|
|
1403
|
-
for r in results
|
|
1404
|
-
]
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
def load_yaml_file(file_path):
|
|
1408
|
-
"""Load a YAML file with error handling"""
|
|
1409
|
-
try:
|
|
1410
|
-
with open(os.path.expanduser(file_path), 'r') as f:
|
|
1411
|
-
return yaml.safe_load(f)
|
|
1412
|
-
except Exception as e:
|
|
1413
|
-
print(f"Error loading YAML file {file_path}: {e}")
|
|
1414
|
-
return None
|
|
1415
|
-
|
|
1416
|
-
def write_yaml_file(file_path, data):
|
|
1417
|
-
"""Write data to a YAML file"""
|
|
1418
|
-
try:
|
|
1419
|
-
with open(os.path.expanduser(file_path), 'w') as f:
|
|
1420
|
-
yaml.dump(data, f)
|
|
1421
|
-
return True
|
|
1422
|
-
except Exception as e:
|
|
1423
|
-
print(f"Error writing YAML file {file_path}: {e}")
|
|
1424
|
-
return False
|
|
1425
|
-
|
|
1426
|
-
def create_or_replace_table(db_path, table_name, data):
|
|
1427
|
-
"""Creates or replaces a table in the SQLite database"""
|
|
1428
|
-
conn = sqlite3.connect(os.path.expanduser(db_path))
|
|
1429
|
-
try:
|
|
1430
|
-
data.to_sql(table_name, conn, if_exists="replace", index=False)
|
|
1431
|
-
print(f"Table '{table_name}' created/replaced successfully.")
|
|
1432
|
-
return True
|
|
1433
|
-
except Exception as e:
|
|
1434
|
-
print(f"Error creating/replacing table '{table_name}': {e}")
|
|
1435
|
-
return False
|
|
1436
|
-
finally:
|
|
1437
|
-
conn.close()
|
|
1438
|
-
|
|
1439
|
-
def find_file_path(filename, search_dirs, suffix=None):
|
|
1440
|
-
"""Find a file in multiple directories"""
|
|
1441
|
-
if suffix and not filename.endswith(suffix):
|
|
1442
|
-
filename += suffix
|
|
2433
|
+
for file_path, content in parsed_files.items():
|
|
2434
|
+
context_parts.append(f"\n--- {file_path} ---")
|
|
2435
|
+
context_parts.append(content)
|
|
2436
|
+
context_parts.append("")
|
|
1443
2437
|
|
|
1444
|
-
|
|
1445
|
-
file_path = os.path.join(os.path.expanduser(dir_path), filename)
|
|
1446
|
-
if os.path.exists(file_path):
|
|
1447
|
-
return file_path
|
|
1448
|
-
|
|
1449
|
-
return None
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
def initialize_npc_project(
|
|
1454
|
-
directory=None,
|
|
1455
|
-
templates=None,
|
|
1456
|
-
context=None,
|
|
1457
|
-
model=None,
|
|
1458
|
-
provider=None,
|
|
1459
|
-
) -> str:
|
|
1460
|
-
"""Initialize an NPC project"""
|
|
1461
|
-
if directory is None:
|
|
1462
|
-
directory = os.getcwd()
|
|
1463
|
-
|
|
1464
|
-
npc_team_dir = os.path.join(directory, "npc_team")
|
|
1465
|
-
os.makedirs(npc_team_dir, exist_ok=True)
|
|
1466
|
-
|
|
1467
|
-
for subdir in ["jinxs", "assembly_lines", "sql_models", "jobs"]:
|
|
1468
|
-
os.makedirs(os.path.join(npc_team_dir, subdir), exist_ok=True)
|
|
1469
|
-
|
|
1470
|
-
forenpc_path = os.path.join(npc_team_dir, "sibiji.npc")
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
# Always ensure default NPC exists
|
|
1474
|
-
if not os.path.exists(forenpc_path):
|
|
1475
|
-
# Use your new NPC class to create sibiji
|
|
1476
|
-
default_npc = {
|
|
1477
|
-
"name": "sibiji",
|
|
1478
|
-
"primary_directive": "You are sibiji, the forenpc of an NPC team...",
|
|
1479
|
-
"model": model or "llama3.2",
|
|
1480
|
-
"provider": provider or "ollama"
|
|
1481
|
-
}
|
|
1482
|
-
with open(forenpc_path, "w") as f:
|
|
1483
|
-
yaml.dump(default_npc, f)
|
|
1484
|
-
|
|
1485
|
-
return f"NPC project initialized in {npc_team_dir}"
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
def execute_jinx_command(
|
|
1492
|
-
jinx: Jinx,
|
|
1493
|
-
args: List[str],
|
|
1494
|
-
messages=None,
|
|
1495
|
-
npc: NPC = None,
|
|
1496
|
-
) -> Dict[str, Any]:
|
|
1497
|
-
"""
|
|
1498
|
-
Execute a jinx command with the given arguments.
|
|
1499
|
-
"""
|
|
1500
|
-
# Extract inputs for the current jinx
|
|
1501
|
-
input_values = extract_jinx_inputs(args, jinx)
|
|
1502
|
-
|
|
1503
|
-
# print(f"Input values: {input_values}")
|
|
1504
|
-
# Execute the jinx with the extracted inputs
|
|
1505
|
-
|
|
1506
|
-
jinx_output = jinx.execute(
|
|
1507
|
-
input_values,
|
|
1508
|
-
jinx.jinx_name,
|
|
1509
|
-
npc=npc,
|
|
1510
|
-
)
|
|
1511
|
-
|
|
1512
|
-
return {"messages": messages, "output": jinx_output}
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
def extract_jinx_inputs(args: List[str], jinx: Jinx) -> Dict[str, Any]:
|
|
1517
|
-
inputs = {}
|
|
1518
|
-
|
|
1519
|
-
# Create flag mapping
|
|
1520
|
-
flag_mapping = {}
|
|
1521
|
-
for input_ in jinx.inputs:
|
|
1522
|
-
if isinstance(input_, str):
|
|
1523
|
-
flag_mapping[f"-{input_[0]}"] = input_
|
|
1524
|
-
flag_mapping[f"--{input_}"] = input_
|
|
1525
|
-
elif isinstance(input_, dict):
|
|
1526
|
-
key = list(input_.keys())[0]
|
|
1527
|
-
flag_mapping[f"-{key[0]}"] = key
|
|
1528
|
-
flag_mapping[f"--{key}"] = key
|
|
1529
|
-
|
|
1530
|
-
# Process arguments
|
|
1531
|
-
used_args = set()
|
|
1532
|
-
for i, arg in enumerate(args):
|
|
1533
|
-
if arg in flag_mapping:
|
|
1534
|
-
# If flag is found, next argument is its value
|
|
1535
|
-
if i + 1 < len(args):
|
|
1536
|
-
input_name = flag_mapping[arg]
|
|
1537
|
-
inputs[input_name] = args[i + 1]
|
|
1538
|
-
used_args.add(i)
|
|
1539
|
-
used_args.add(i + 1)
|
|
1540
|
-
else:
|
|
1541
|
-
print(f"Warning: {arg} flag is missing a value.")
|
|
1542
|
-
|
|
1543
|
-
# If no flags used, combine remaining args for first input
|
|
1544
|
-
unused_args = [arg for i, arg in enumerate(args) if i not in used_args]
|
|
1545
|
-
if unused_args and jinx.inputs:
|
|
1546
|
-
first_input = jinx.inputs[0]
|
|
1547
|
-
if isinstance(first_input, str):
|
|
1548
|
-
inputs[first_input] = " ".join(unused_args)
|
|
1549
|
-
elif isinstance(first_input, dict):
|
|
1550
|
-
key = list(first_input.keys())[0]
|
|
1551
|
-
inputs[key] = " ".join(unused_args)
|
|
1552
|
-
|
|
1553
|
-
# Add default values for inputs not provided
|
|
1554
|
-
for input_ in jinx.inputs:
|
|
1555
|
-
if isinstance(input_, str):
|
|
1556
|
-
if input_ not in inputs:
|
|
1557
|
-
if any(args): # If we have any arguments at all
|
|
1558
|
-
raise ValueError(f"Missing required input: {input_}")
|
|
1559
|
-
else:
|
|
1560
|
-
inputs[input_] = None # Allow None for completely empty calls
|
|
1561
|
-
elif isinstance(input_, dict):
|
|
1562
|
-
key = list(input_.keys())[0]
|
|
1563
|
-
if key not in inputs:
|
|
1564
|
-
inputs[key] = input_[key]
|
|
1565
|
-
|
|
1566
|
-
return inputs
|
|
1567
|
-
|
|
2438
|
+
return "\n".join(context_parts)
|