npcpy 1.1.28__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/data/audio.py +16 -38
- npcpy/data/image.py +29 -29
- npcpy/data/load.py +4 -3
- npcpy/data/text.py +28 -28
- npcpy/data/video.py +6 -6
- npcpy/data/web.py +49 -21
- npcpy/ft/__init__.py +0 -0
- 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 +37 -15
- npcpy/gen/response.py +287 -111
- npcpy/gen/video_gen.py +10 -9
- npcpy/llm_funcs.py +447 -79
- npcpy/memory/command_history.py +201 -48
- npcpy/memory/kg_vis.py +74 -74
- npcpy/memory/knowledge_graph.py +482 -115
- npcpy/memory/memory_processor.py +81 -0
- npcpy/memory/search.py +70 -70
- npcpy/mix/debate.py +192 -3
- npcpy/npc_compiler.py +1541 -879
- npcpy/npc_sysenv.py +250 -78
- npcpy/serve.py +1036 -321
- 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 +20 -20
- npcpy/work/plan.py +8 -8
- npcpy/work/trigger.py +3 -3
- {npcpy-1.1.28.dist-info → npcpy-1.2.32.dist-info}/METADATA +169 -9
- npcpy-1.2.32.dist-info/RECORD +54 -0
- npcpy-1.1.28.dist-info/RECORD +0 -40
- {npcpy-1.1.28.dist-info → npcpy-1.2.32.dist-info}/WHEEL +0 -0
- {npcpy-1.1.28.dist-info → npcpy-1.2.32.dist-info}/licenses/LICENSE +0 -0
- {npcpy-1.1.28.dist-info → npcpy-1.2.32.dist-info}/top_level.txt +0 -0
npcpy/npc_compiler.py
CHANGED
|
@@ -26,7 +26,7 @@ from npcpy.npc_sysenv import (
|
|
|
26
26
|
get_system_message,
|
|
27
27
|
|
|
28
28
|
)
|
|
29
|
-
from npcpy.memory.command_history import CommandHistory
|
|
29
|
+
from npcpy.memory.command_history import CommandHistory, generate_message_id
|
|
30
30
|
|
|
31
31
|
class SilentUndefined(Undefined):
|
|
32
32
|
def _fail_with_undefined_error(self, *args, **kwargs):
|
|
@@ -36,6 +36,199 @@ import math
|
|
|
36
36
|
from PIL import Image
|
|
37
37
|
|
|
38
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
|
+
|
|
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
|
|
91
|
+
|
|
92
|
+
|
|
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
|
|
230
|
+
|
|
231
|
+
|
|
39
232
|
class Jinx:
|
|
40
233
|
'''
|
|
41
234
|
|
|
@@ -71,7 +264,6 @@ class Jinx:
|
|
|
71
264
|
self.inputs = jinx_data.get("inputs", [])
|
|
72
265
|
self.description = jinx_data.get("description", "")
|
|
73
266
|
self.steps = self._parse_steps(jinx_data.get("steps", []))
|
|
74
|
-
|
|
75
267
|
def _parse_steps(self, steps):
|
|
76
268
|
"""Parse steps from jinx definition"""
|
|
77
269
|
parsed_steps = []
|
|
@@ -82,66 +274,71 @@ class Jinx:
|
|
|
82
274
|
"engine": step.get("engine", "natural"),
|
|
83
275
|
"code": step.get("code", "")
|
|
84
276
|
}
|
|
277
|
+
if "mode" in step:
|
|
278
|
+
parsed_step["mode"] = step["mode"]
|
|
85
279
|
parsed_steps.append(parsed_step)
|
|
86
280
|
else:
|
|
87
281
|
raise ValueError(f"Invalid step format: {step}")
|
|
88
282
|
return parsed_steps
|
|
89
|
-
|
|
283
|
+
|
|
90
284
|
def execute(self,
|
|
91
|
-
input_values,
|
|
92
|
-
jinxs_dict,
|
|
93
|
-
jinja_env = None,
|
|
94
|
-
npc = None,
|
|
95
|
-
messages=None
|
|
96
|
-
|
|
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
|
+
"""
|
|
97
295
|
if jinja_env is None:
|
|
98
|
-
# For standalone jinx execution, we don't need FileSystemLoader
|
|
99
|
-
# Just use a basic environment with no file system dependencies
|
|
100
296
|
from jinja2 import DictLoader
|
|
101
297
|
jinja_env = Environment(
|
|
102
|
-
loader=DictLoader({}),
|
|
298
|
+
loader=DictLoader({}),
|
|
103
299
|
undefined=SilentUndefined,
|
|
104
300
|
)
|
|
105
|
-
|
|
106
|
-
context = (npc.shared_context.copy() if npc else {})
|
|
301
|
+
|
|
302
|
+
context = (npc.shared_context.copy() if npc and hasattr(npc, 'shared_context') else {})
|
|
107
303
|
context.update(input_values)
|
|
108
304
|
context.update({
|
|
109
305
|
"jinxs": jinxs_dict,
|
|
110
306
|
"llm_response": None,
|
|
111
|
-
"output": None,
|
|
307
|
+
"output": None,
|
|
112
308
|
"messages": messages,
|
|
113
309
|
})
|
|
114
310
|
|
|
115
|
-
#
|
|
311
|
+
# This is the key change: Extract 'extra_globals' from kwargs
|
|
312
|
+
extra_globals = kwargs.get('extra_globals')
|
|
313
|
+
|
|
116
314
|
for i, step in enumerate(self.steps):
|
|
117
315
|
context = self._execute_step(
|
|
118
|
-
step,
|
|
316
|
+
step,
|
|
119
317
|
context,
|
|
120
|
-
jinja_env,
|
|
121
|
-
npc=npc,
|
|
122
|
-
messages=messages,
|
|
123
|
-
|
|
124
|
-
)
|
|
318
|
+
jinja_env,
|
|
319
|
+
npc=npc,
|
|
320
|
+
messages=messages,
|
|
321
|
+
extra_globals=extra_globals # Pass it down to the step executor
|
|
322
|
+
)
|
|
125
323
|
|
|
126
324
|
return context
|
|
127
|
-
|
|
325
|
+
|
|
128
326
|
def _execute_step(self,
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
):
|
|
135
|
-
"""
|
|
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
|
+
"""
|
|
136
336
|
engine = step.get("engine", "natural")
|
|
137
337
|
code = step.get("code", "")
|
|
138
338
|
step_name = step.get("name", "unnamed_step")
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
339
|
+
mode = step.get("mode", "chat")
|
|
142
340
|
|
|
143
341
|
try:
|
|
144
|
-
#print(code)
|
|
145
342
|
template = jinja_env.from_string(code)
|
|
146
343
|
rendered_code = template.render(**context)
|
|
147
344
|
|
|
@@ -153,24 +350,32 @@ class Jinx:
|
|
|
153
350
|
rendered_code = code
|
|
154
351
|
rendered_engine = engine
|
|
155
352
|
|
|
156
|
-
# Execute based on engine type
|
|
157
353
|
if rendered_engine == "natural":
|
|
158
354
|
if rendered_code.strip():
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
+
|
|
166
370
|
response_text = response.get("response", "")
|
|
167
371
|
context['output'] = response_text
|
|
168
372
|
context["llm_response"] = response_text
|
|
169
373
|
context["results"] = response_text
|
|
170
374
|
context[step_name] = response_text
|
|
171
375
|
context['messages'] = response.get('messages')
|
|
376
|
+
|
|
172
377
|
elif rendered_engine == "python":
|
|
173
|
-
#
|
|
378
|
+
# Base globals available to all python jinxes, defined within the library (npcpy)
|
|
174
379
|
exec_globals = {
|
|
175
380
|
"__builtins__": __builtins__,
|
|
176
381
|
"npc": npc,
|
|
@@ -185,49 +390,57 @@ class Jinx:
|
|
|
185
390
|
"fnmatch": fnmatch,
|
|
186
391
|
"pathlib": pathlib,
|
|
187
392
|
"subprocess": subprocess,
|
|
188
|
-
"get_llm_response": npy.llm_funcs.get_llm_response,
|
|
189
|
-
|
|
190
|
-
|
|
393
|
+
"get_llm_response": npy.llm_funcs.get_llm_response,
|
|
394
|
+
"CommandHistory": CommandHistory, # This is fine, it's part of npcpy
|
|
395
|
+
}
|
|
191
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)
|
|
192
400
|
|
|
193
|
-
# Execute the code
|
|
194
401
|
exec_locals = {}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
+
|
|
198
410
|
context.update(exec_locals)
|
|
199
411
|
|
|
200
|
-
# Handle explicit output
|
|
201
412
|
if "output" in exec_locals:
|
|
202
413
|
outp = exec_locals["output"]
|
|
203
414
|
context["output"] = outp
|
|
204
415
|
context[step_name] = outp
|
|
205
|
-
messages
|
|
206
|
-
|
|
207
|
-
|
|
416
|
+
if messages is not None:
|
|
417
|
+
messages.append({'role':'assistant',
|
|
418
|
+
'content': f'Jinx executed with following output: {outp}'})
|
|
419
|
+
context['messages'] = messages
|
|
208
420
|
|
|
209
421
|
else:
|
|
210
|
-
# Handle unknown engine
|
|
211
422
|
context[step_name] = {"error": f"Unsupported engine: {rendered_engine}"}
|
|
212
423
|
|
|
213
424
|
return context
|
|
214
|
-
|
|
215
425
|
def to_dict(self):
|
|
216
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
|
+
|
|
217
438
|
return {
|
|
218
439
|
"jinx_name": self.jinx_name,
|
|
219
440
|
"description": self.description,
|
|
220
441
|
"inputs": self.inputs,
|
|
221
|
-
"steps":
|
|
222
|
-
{
|
|
223
|
-
"name": step.get("name", f"step_{i}"),
|
|
224
|
-
"engine": step.get("engine"),
|
|
225
|
-
"code": step.get("code")
|
|
226
|
-
}
|
|
227
|
-
for i, step in enumerate(self.steps)
|
|
228
|
-
]
|
|
442
|
+
"steps": steps_list
|
|
229
443
|
}
|
|
230
|
-
|
|
231
444
|
def save(self, directory):
|
|
232
445
|
"""Save jinx to file"""
|
|
233
446
|
jinx_path = os.path.join(directory, f"{self.jinx_name}.jinx")
|
|
@@ -237,19 +450,19 @@ class Jinx:
|
|
|
237
450
|
@classmethod
|
|
238
451
|
def from_mcp(cls, mcp_tool):
|
|
239
452
|
"""Convert an MCP tool to NPC jinx format"""
|
|
240
|
-
|
|
453
|
+
|
|
241
454
|
try:
|
|
242
455
|
import inspect
|
|
243
456
|
|
|
244
|
-
|
|
457
|
+
|
|
245
458
|
doc = mcp_tool.__doc__ or ""
|
|
246
459
|
name = mcp_tool.__name__
|
|
247
460
|
signature = inspect.signature(mcp_tool)
|
|
248
461
|
|
|
249
|
-
|
|
462
|
+
|
|
250
463
|
inputs = []
|
|
251
464
|
for param_name, param in signature.parameters.items():
|
|
252
|
-
if param_name != 'self':
|
|
465
|
+
if param_name != 'self':
|
|
253
466
|
param_type = param.annotation if param.annotation != inspect.Parameter.empty else None
|
|
254
467
|
param_default = None if param.default == inspect.Parameter.empty else param.default
|
|
255
468
|
|
|
@@ -259,7 +472,7 @@ class Jinx:
|
|
|
259
472
|
"default": param_default
|
|
260
473
|
})
|
|
261
474
|
|
|
262
|
-
|
|
475
|
+
|
|
263
476
|
jinx_data = {
|
|
264
477
|
"jinx_name": name,
|
|
265
478
|
"description": doc.strip(),
|
|
@@ -269,7 +482,7 @@ class Jinx:
|
|
|
269
482
|
"name": "mcp_function_call",
|
|
270
483
|
"engine": "python",
|
|
271
484
|
"code": f"""
|
|
272
|
-
|
|
485
|
+
|
|
273
486
|
import {mcp_tool.__module__}
|
|
274
487
|
output = {mcp_tool.__module__}.{name}(
|
|
275
488
|
{', '.join([f'{inp["name"]}=context.get("{inp["name"]}")' for inp in inputs])}
|
|
@@ -283,135 +496,83 @@ output = {mcp_tool.__module__}.{name}(
|
|
|
283
496
|
|
|
284
497
|
except:
|
|
285
498
|
pass
|
|
499
|
+
|
|
286
500
|
def load_jinxs_from_directory(directory):
|
|
287
|
-
"""Load all jinxs from a directory"""
|
|
501
|
+
"""Load all jinxs from a directory recursively"""
|
|
288
502
|
jinxs = []
|
|
289
503
|
directory = os.path.expanduser(directory)
|
|
290
504
|
|
|
291
505
|
if not os.path.exists(directory):
|
|
292
506
|
return jinxs
|
|
293
|
-
|
|
294
|
-
for
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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}")
|
|
302
517
|
|
|
303
518
|
return jinxs
|
|
304
|
-
def agent_pass_handler(command, extracted_data, **kwargs):
|
|
305
|
-
"""Handler for agent pass action"""
|
|
306
|
-
npc = kwargs.get('npc')
|
|
307
|
-
team = kwargs.get('team')
|
|
308
|
-
|
|
309
|
-
# If team isn't in kwargs, try to get it from the npc's context
|
|
310
|
-
if not team and npc and hasattr(npc, '_current_team'):
|
|
311
|
-
team = npc._current_team
|
|
312
|
-
|
|
313
|
-
print(f"DEBUG agent_pass_handler: npc={npc.name if npc else None}, team={team.name if team else None}")
|
|
314
|
-
|
|
315
|
-
if not npc or not team:
|
|
316
|
-
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"}
|
|
317
|
-
|
|
318
|
-
target_npc_name = extracted_data.get('target_npc')
|
|
319
|
-
if not target_npc_name:
|
|
320
|
-
return {"messages": kwargs.get('messages', []), "output": "Error: No target NPC specified"}
|
|
321
|
-
|
|
322
|
-
# PREVENT INFINITE LOOPS: Check if we're passing back to the same NPC
|
|
323
|
-
messages = kwargs.get('messages', [])
|
|
324
|
-
|
|
325
|
-
# Count how many times this has been passed around
|
|
326
|
-
pass_count = 0
|
|
327
|
-
recent_passes = []
|
|
328
|
-
|
|
329
|
-
for msg in messages[-10:]: # Check last 10 messages
|
|
330
|
-
if 'NOTE: THIS COMMAND HAS BEEN PASSED FROM' in msg.get('content', ''):
|
|
331
|
-
pass_count += 1
|
|
332
|
-
# Extract who passed it
|
|
333
|
-
if 'PASSED FROM' in msg.get('content', ''):
|
|
334
|
-
content = msg.get('content', '')
|
|
335
|
-
if 'PASSED FROM' in content and 'TO YOU' in content:
|
|
336
|
-
parts = content.split('PASSED FROM')[1].split('TO YOU')[0].strip()
|
|
337
|
-
recent_passes.append(parts)
|
|
338
|
-
|
|
339
|
-
print(f"DEBUG: Pass count: {pass_count}, Recent passes: {recent_passes}")
|
|
340
|
-
|
|
341
|
-
# If we've been passing this around too much, force current NPC to handle it
|
|
342
|
-
if pass_count >= 3:
|
|
343
|
-
return {
|
|
344
|
-
"messages": kwargs.get('messages', []),
|
|
345
|
-
"output": f"Task has been passed around {pass_count} times. {npc.name} will handle it directly.\n\n" +
|
|
346
|
-
"I'll create a simple document as requested. Here's a basic document structure:\n\n" +
|
|
347
|
-
"# Simple Document\n\n" +
|
|
348
|
-
"## Introduction\n" +
|
|
349
|
-
"This document was created as requested.\n\n" +
|
|
350
|
-
"## Content\n" +
|
|
351
|
-
"Document content goes here.\n\n" +
|
|
352
|
-
"## Conclusion\n" +
|
|
353
|
-
"Document completed successfully."
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
# Check if we're trying to pass to ourselves (immediate loop)
|
|
357
|
-
if target_npc_name == npc.name:
|
|
358
|
-
return {
|
|
359
|
-
"messages": kwargs.get('messages', []),
|
|
360
|
-
"output": f"Cannot pass task to myself ({npc.name}). I'll handle this directly.\n\n" +
|
|
361
|
-
f"Creating a simple document as requested:\n\n" +
|
|
362
|
-
f"# Simple Document\n\n" +
|
|
363
|
-
f"This document has been created by {npc.name}.\n" +
|
|
364
|
-
f"Content and structure provided as requested."
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
print(f"DEBUG: Looking for target NPC: {target_npc_name}")
|
|
368
|
-
|
|
369
|
-
# Get target NPC from team
|
|
370
|
-
target_npc = team.get_npc(target_npc_name)
|
|
371
|
-
if not target_npc:
|
|
372
|
-
available_npcs = list(team.npcs.keys()) if hasattr(team, 'npcs') else []
|
|
373
|
-
return {"messages": kwargs.get('messages', []), "output": f"Error: NPC '{target_npc_name}' not found in team. Available: {available_npcs}"}
|
|
374
|
-
|
|
375
|
-
print(f"DEBUG: Found target NPC: {target_npc.name}")
|
|
376
|
-
|
|
377
|
-
# Use handle_agent_pass
|
|
378
|
-
result = npc.handle_agent_pass(
|
|
379
|
-
target_npc,
|
|
380
|
-
command,
|
|
381
|
-
messages=kwargs.get('messages'),
|
|
382
|
-
context=kwargs.get('context'),
|
|
383
|
-
shared_context=getattr(team, 'shared_context', None),
|
|
384
|
-
stream=kwargs.get('stream', False),
|
|
385
|
-
team=team
|
|
386
|
-
)
|
|
387
|
-
|
|
388
|
-
print(f"DEBUG: Agent pass result: {type(result)}")
|
|
389
|
-
return result
|
|
390
519
|
|
|
391
|
-
# NPC-specific action space that extends the default
|
|
392
520
|
def get_npc_action_space(npc=None, team=None):
|
|
393
|
-
"""Get action space for NPC including
|
|
521
|
+
"""Get action space for NPC including memory CRUD and core capabilities"""
|
|
394
522
|
actions = DEFAULT_ACTION_SPACE.copy()
|
|
395
523
|
|
|
396
|
-
|
|
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
|
+
|
|
397
562
|
if team and hasattr(team, 'npcs') and len(team.npcs) > 1:
|
|
398
563
|
available_npcs = [name for name in team.npcs.keys() if name != (npc.name if npc else None)]
|
|
399
564
|
|
|
400
|
-
# Create a closure that captures the team reference
|
|
401
565
|
def team_aware_handler(command, extracted_data, **kwargs):
|
|
402
|
-
# Inject the team into kwargs if it's missing
|
|
403
566
|
if 'team' not in kwargs or kwargs['team'] is None:
|
|
404
567
|
kwargs['team'] = team
|
|
405
568
|
return agent_pass_handler(command, extracted_data, **kwargs)
|
|
406
569
|
|
|
407
570
|
actions["pass_to_npc"] = {
|
|
408
|
-
"description": "Pass
|
|
571
|
+
"description": "Pass request to another NPC - only when task requires their specific expertise",
|
|
409
572
|
"handler": team_aware_handler,
|
|
410
573
|
"context": lambda npc=npc, team=team, **_: (
|
|
411
|
-
f"Use this SPARINGLY when the request absolutely requires another team member's expertise. "
|
|
412
574
|
f"Available NPCs: {', '.join(available_npcs)}. "
|
|
413
|
-
f"
|
|
414
|
-
f"Only pass when you genuinely cannot complete the task due to lack of domain expertise."
|
|
575
|
+
f"Only pass when you genuinely cannot complete the task."
|
|
415
576
|
),
|
|
416
577
|
"output_keys": {
|
|
417
578
|
"target_npc": {
|
|
@@ -422,6 +583,103 @@ def get_npc_action_space(npc=None, team=None):
|
|
|
422
583
|
}
|
|
423
584
|
|
|
424
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
|
|
682
|
+
|
|
425
683
|
class NPC:
|
|
426
684
|
def __init__(
|
|
427
685
|
self,
|
|
@@ -438,6 +696,7 @@ class NPC:
|
|
|
438
696
|
api_key: str = None,
|
|
439
697
|
db_conn=None,
|
|
440
698
|
use_global_jinxs=False,
|
|
699
|
+
memory = False,
|
|
441
700
|
**kwargs
|
|
442
701
|
):
|
|
443
702
|
"""
|
|
@@ -468,22 +727,19 @@ class NPC:
|
|
|
468
727
|
self.provider = provider
|
|
469
728
|
self.api_url = api_url
|
|
470
729
|
self.api_key = api_key
|
|
471
|
-
|
|
472
|
-
#for these cases
|
|
473
|
-
# if npcsh is initialized, use the ~/.npcsh/npc_team
|
|
474
|
-
# otherwise imply
|
|
730
|
+
|
|
475
731
|
if use_global_jinxs:
|
|
476
732
|
self.jinxs_directory = os.path.expanduser('~/.npcsh/npc_team/jinxs/')
|
|
477
733
|
else:
|
|
478
734
|
self.jinxs_directory = None
|
|
479
|
-
self.npc_directory = None
|
|
480
|
-
# keep the jinxs tho to enable easieros.path.abspath('./npc_team/')
|
|
735
|
+
self.npc_directory = None
|
|
481
736
|
|
|
737
|
+
self.team = team
|
|
482
738
|
if tools is not None:
|
|
483
739
|
tools_schema, tool_map = auto_tools(tools)
|
|
484
|
-
self.tools = tools_schema
|
|
485
|
-
self.tool_map = tool_map
|
|
486
|
-
self.tools_schema = tools_schema
|
|
740
|
+
self.tools = tools_schema
|
|
741
|
+
self.tool_map = tool_map
|
|
742
|
+
self.tools_schema = tools_schema
|
|
487
743
|
else:
|
|
488
744
|
self.tools = []
|
|
489
745
|
self.tool_map = {}
|
|
@@ -506,42 +762,271 @@ class NPC:
|
|
|
506
762
|
undefined=SilentUndefined,
|
|
507
763
|
)
|
|
508
764
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
if
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
self.
|
|
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
|
+
|
|
773
|
+
if self.db_conn:
|
|
774
|
+
self._setup_db()
|
|
775
|
+
self.command_history = CommandHistory(db=self.db_conn)
|
|
776
|
+
if memory:
|
|
777
|
+
self.kg_data = self._load_npc_kg()
|
|
778
|
+
self.memory = self.get_memory_context()
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
self.jinxs = self._load_npc_jinxs(jinxs or "*")
|
|
783
|
+
|
|
784
|
+
self.shared_context = {
|
|
785
|
+
"dataframes": {},
|
|
786
|
+
"current_data": None,
|
|
787
|
+
"computation_results": [],
|
|
788
|
+
"memories":{}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
for key, value in kwargs.items():
|
|
792
|
+
setattr(self, key, value)
|
|
793
|
+
|
|
794
|
+
if db_conn is not None:
|
|
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
|
+
)
|
|
519
961
|
|
|
962
|
+
messages = response.get('messages', messages)
|
|
520
963
|
|
|
521
|
-
|
|
522
|
-
|
|
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}
|
|
523
986
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
|
530
1006
|
}
|
|
531
1007
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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)
|
|
535
1014
|
|
|
536
|
-
|
|
537
|
-
|
|
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
|
+
|
|
538
1024
|
def _load_npc_memory(self):
|
|
1025
|
+
"""Enhanced memory loading that includes KG context"""
|
|
539
1026
|
memory = self.command_history.get_messages_by_npc(self.name, n_last=self.memory_length)
|
|
540
|
-
#import pdb
|
|
541
|
-
#pdb.set_trace()
|
|
542
1027
|
memory = [{'role':mem['role'], 'content':mem['content']} for mem in memory]
|
|
543
|
-
|
|
544
1028
|
return memory
|
|
1029
|
+
|
|
545
1030
|
def _load_from_file(self, file):
|
|
546
1031
|
"""Load NPC configuration from file"""
|
|
547
1032
|
if "~" in file:
|
|
@@ -553,19 +1038,15 @@ class NPC:
|
|
|
553
1038
|
if not npc_data:
|
|
554
1039
|
raise ValueError(f"Failed to load NPC from {file}")
|
|
555
1040
|
|
|
556
|
-
# Extract core fields
|
|
557
1041
|
self.name = npc_data.get("name")
|
|
558
1042
|
if not self.name:
|
|
559
|
-
# Fall back to filename if name not in file
|
|
560
1043
|
self.name = os.path.splitext(os.path.basename(file))[0]
|
|
561
1044
|
|
|
562
1045
|
self.primary_directive = npc_data.get("primary_directive")
|
|
563
1046
|
|
|
564
|
-
# Handle wildcard jinxs specification
|
|
565
1047
|
jinxs_spec = npc_data.get("jinxs", "*")
|
|
566
|
-
|
|
1048
|
+
|
|
567
1049
|
if jinxs_spec == "*":
|
|
568
|
-
# Will be loaded in _load_npc_jinxs
|
|
569
1050
|
self.jinxs_spec = "*"
|
|
570
1051
|
else:
|
|
571
1052
|
self.jinxs_spec = jinxs_spec
|
|
@@ -576,137 +1057,574 @@ class NPC:
|
|
|
576
1057
|
self.api_key = npc_data.get("api_key")
|
|
577
1058
|
self.name = npc_data.get("name", self.name)
|
|
578
1059
|
|
|
579
|
-
# Store path for future reference
|
|
580
1060
|
self.npc_path = file
|
|
581
|
-
|
|
582
|
-
# Set NPC-specific jinxs directory path
|
|
583
1061
|
self.npc_jinxs_directory = os.path.join(os.path.dirname(file), "jinxs")
|
|
1062
|
+
|
|
584
1063
|
def get_system_prompt(self, simple=False):
|
|
1064
|
+
"""Get system prompt for the NPC"""
|
|
585
1065
|
if simple or self.plain_system_message:
|
|
586
1066
|
return self.primary_directive
|
|
587
1067
|
else:
|
|
588
|
-
|
|
589
1068
|
return get_system_message(self, team=self.team)
|
|
1069
|
+
|
|
590
1070
|
def _setup_db(self):
|
|
591
1071
|
"""Set up database tables and determine type"""
|
|
592
|
-
|
|
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"
|
|
593
1090
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
result = conn.execute(text("""
|
|
599
|
-
SELECT table_name, obj_description((quote_ident(table_name))::regclass, 'pg_class')
|
|
600
|
-
FROM information_schema.tables
|
|
601
|
-
WHERE table_schema='public';
|
|
602
|
-
"""))
|
|
603
|
-
self.tables = result.fetchall()
|
|
604
|
-
self.db_type = "postgres"
|
|
605
|
-
|
|
606
|
-
elif dialect == "sqlite":
|
|
607
|
-
result = conn.execute(text(
|
|
608
|
-
"SELECT name, sql FROM sqlite_master WHERE type='table';"
|
|
609
|
-
))
|
|
610
|
-
self.tables = result.fetchall()
|
|
611
|
-
self.db_type = "sqlite"
|
|
612
|
-
|
|
613
|
-
else:
|
|
614
|
-
print(f"Unsupported DB dialect: {dialect}")
|
|
615
|
-
self.tables = None
|
|
616
|
-
self.db_type = None
|
|
1091
|
+
else:
|
|
1092
|
+
print(f"Unsupported DB dialect: {dialect}")
|
|
1093
|
+
self.tables = None
|
|
1094
|
+
self.db_type = None
|
|
617
1095
|
|
|
618
|
-
except Exception as e:
|
|
619
|
-
print(f"Error setting up database: {e}")
|
|
620
|
-
self.tables = None
|
|
621
|
-
self.db_type = None
|
|
622
1096
|
def _load_npc_jinxs(self, jinxs):
|
|
623
1097
|
"""Load and process NPC-specific jinxs"""
|
|
624
1098
|
npc_jinxs = []
|
|
625
1099
|
|
|
626
|
-
if self.jinxs_directory is None:
|
|
627
|
-
self.jinxs_dict = {}
|
|
628
|
-
return None
|
|
629
|
-
# Handle wildcard case - load all jinxs from the jinxs directory
|
|
630
1100
|
if jinxs == "*":
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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
|
+
|
|
639
1109
|
self.jinxs_dict = {jinx.jinx_name: jinx for jinx in npc_jinxs}
|
|
640
|
-
#print(npc_jinxs)
|
|
641
1110
|
return npc_jinxs
|
|
642
|
-
|
|
643
1111
|
|
|
644
1112
|
for jinx in jinxs:
|
|
645
|
-
#need to add a block here for mcp jinxs.
|
|
646
|
-
|
|
647
1113
|
if isinstance(jinx, Jinx):
|
|
648
1114
|
npc_jinxs.append(jinx)
|
|
649
1115
|
elif isinstance(jinx, dict):
|
|
650
1116
|
npc_jinxs.append(Jinx(jinx_data=jinx))
|
|
651
|
-
|
|
652
|
-
# Try to load from file
|
|
1117
|
+
elif isinstance(jinx, str):
|
|
653
1118
|
jinx_path = None
|
|
654
1119
|
jinx_name = jinx
|
|
655
1120
|
if not jinx_name.endswith(".jinx"):
|
|
656
1121
|
jinx_name += ".jinx"
|
|
657
1122
|
|
|
658
|
-
|
|
659
|
-
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):
|
|
660
1124
|
candidate_path = os.path.join(self.jinxs_directory, jinx_name)
|
|
661
1125
|
if os.path.exists(candidate_path):
|
|
662
1126
|
jinx_path = candidate_path
|
|
663
1127
|
|
|
664
1128
|
if jinx_path:
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
npc_jinxs.append(jinx_obj)
|
|
668
|
-
except Exception as e:
|
|
669
|
-
print(f"Error loading jinx {jinx_path}: {e}")
|
|
1129
|
+
jinx_obj = Jinx(jinx_path=jinx_path)
|
|
1130
|
+
npc_jinxs.append(jinx_obj)
|
|
670
1131
|
|
|
671
|
-
# Update jinxs dictionary
|
|
672
1132
|
self.jinxs_dict = {jinx.jinx_name: jinx for jinx in npc_jinxs}
|
|
1133
|
+
print(npc_jinxs)
|
|
673
1134
|
return npc_jinxs
|
|
674
|
-
|
|
675
1135
|
def get_llm_response(self,
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
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"
|
|
690
1195
|
|
|
691
1196
|
response = npy.llm_funcs.get_llm_response(
|
|
692
1197
|
request,
|
|
693
|
-
model=self.model,
|
|
694
|
-
provider=self.provider,
|
|
695
1198
|
npc=self,
|
|
696
1199
|
jinxs=jinxs,
|
|
697
|
-
tools=
|
|
698
|
-
tool_map=
|
|
1200
|
+
tools=final_tools_schema,
|
|
1201
|
+
tool_map=final_tool_map_dict,
|
|
699
1202
|
tool_choice=tool_choice,
|
|
700
1203
|
auto_process_tool_calls=auto_process_tool_calls,
|
|
701
1204
|
messages=self.memory if messages is None else messages,
|
|
702
1205
|
**kwargs
|
|
703
1206
|
)
|
|
704
|
-
|
|
1207
|
+
|
|
705
1208
|
return response
|
|
706
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
|
+
|
|
707
1625
|
def execute_jinx(self, jinx_name, inputs, conversation_id=None, message_id=None, team_name=None):
|
|
708
1626
|
"""Execute a jinx by name"""
|
|
709
|
-
|
|
1627
|
+
|
|
710
1628
|
if jinx_name in self.jinxs_dict:
|
|
711
1629
|
jinx = self.jinxs_dict[jinx_name]
|
|
712
1630
|
elif jinx_name in self.jinxs_dict:
|
|
@@ -734,24 +1652,22 @@ class NPC:
|
|
|
734
1652
|
team_name=team_name,
|
|
735
1653
|
)
|
|
736
1654
|
return result
|
|
1655
|
+
|
|
737
1656
|
def check_llm_command(self,
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
1657
|
+
command,
|
|
1658
|
+
messages=None,
|
|
1659
|
+
context=None,
|
|
1660
|
+
team=None,
|
|
1661
|
+
stream=False):
|
|
743
1662
|
"""Check if a command is for the LLM"""
|
|
744
1663
|
if context is None:
|
|
745
1664
|
context = self.shared_context
|
|
746
1665
|
|
|
747
|
-
# Store team reference on NPC for handler access
|
|
748
1666
|
if team:
|
|
749
1667
|
self._current_team = team
|
|
750
1668
|
|
|
751
|
-
# Get NPC-specific action space
|
|
752
1669
|
actions = get_npc_action_space(npc=self, team=team)
|
|
753
1670
|
|
|
754
|
-
# Call the LLM command checker with NPC-specific actions
|
|
755
1671
|
return npy.llm_funcs.check_llm_command(
|
|
756
1672
|
command,
|
|
757
1673
|
model=self.model,
|
|
@@ -765,13 +1681,13 @@ class NPC:
|
|
|
765
1681
|
)
|
|
766
1682
|
|
|
767
1683
|
def handle_agent_pass(self,
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
1684
|
+
npc_to_pass,
|
|
1685
|
+
command,
|
|
1686
|
+
messages=None,
|
|
1687
|
+
context=None,
|
|
1688
|
+
shared_context=None,
|
|
1689
|
+
stream=False,
|
|
1690
|
+
team=None):
|
|
775
1691
|
"""Pass a command to another NPC"""
|
|
776
1692
|
print('handling agent pass')
|
|
777
1693
|
if isinstance(npc_to_pass, NPC):
|
|
@@ -779,19 +1695,16 @@ class NPC:
|
|
|
779
1695
|
else:
|
|
780
1696
|
return {"error": "Invalid NPC to pass command to"}
|
|
781
1697
|
|
|
782
|
-
# Update shared context
|
|
783
1698
|
if shared_context is not None:
|
|
784
1699
|
self.shared_context.update(shared_context)
|
|
785
1700
|
target_npc.shared_context.update(shared_context)
|
|
786
1701
|
|
|
787
|
-
# Add a note that this command was passed from another NPC
|
|
788
1702
|
updated_command = (
|
|
789
1703
|
command
|
|
790
1704
|
+ "\n\n"
|
|
791
1705
|
+ f"NOTE: THIS COMMAND HAS BEEN PASSED FROM {self.name} TO YOU, {target_npc.name}.\n"
|
|
792
1706
|
+ "PLEASE CHOOSE ONE OF THE OTHER OPTIONS WHEN RESPONDING."
|
|
793
1707
|
)
|
|
794
|
-
|
|
795
1708
|
|
|
796
1709
|
result = target_npc.check_llm_command(
|
|
797
1710
|
updated_command,
|
|
@@ -841,15 +1754,195 @@ class NPC:
|
|
|
841
1754
|
str_rep += f" - {jinx.jinx_name}\n"
|
|
842
1755
|
else:
|
|
843
1756
|
str_rep += "No jinxs available.\n"
|
|
844
|
-
return str_rep
|
|
845
|
-
|
|
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
|
+
|
|
1936
|
+
|
|
846
1937
|
class Team:
|
|
847
1938
|
def __init__(self,
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
1939
|
+
team_path=None,
|
|
1940
|
+
npcs=None,
|
|
1941
|
+
forenpc=None,
|
|
1942
|
+
jinxs=None,
|
|
1943
|
+
db_conn=None,
|
|
1944
|
+
model = None,
|
|
1945
|
+
provider = None):
|
|
853
1946
|
"""
|
|
854
1947
|
Initialize an NPC team from directory or list of NPCs
|
|
855
1948
|
|
|
@@ -858,6 +1951,9 @@ class Team:
|
|
|
858
1951
|
npcs: List of NPC objects
|
|
859
1952
|
db_conn: Database connection
|
|
860
1953
|
"""
|
|
1954
|
+
self.model = model
|
|
1955
|
+
self.provider = provider
|
|
1956
|
+
|
|
861
1957
|
self.npcs = {}
|
|
862
1958
|
self.sub_teams = {}
|
|
863
1959
|
self.jinxs_dict = jinxs or {}
|
|
@@ -880,63 +1976,89 @@ class Team:
|
|
|
880
1976
|
"dataframes": {},
|
|
881
1977
|
"memories": {},
|
|
882
1978
|
"execution_history": [],
|
|
883
|
-
"npc_messages": {}
|
|
1979
|
+
"npc_messages": {},
|
|
1980
|
+
"context":''
|
|
884
1981
|
}
|
|
885
1982
|
|
|
886
1983
|
if team_path:
|
|
887
|
-
|
|
888
1984
|
self._load_from_directory()
|
|
889
1985
|
|
|
890
1986
|
elif npcs:
|
|
891
1987
|
for npc in npcs:
|
|
892
1988
|
self.npcs[npc.name] = npc
|
|
893
|
-
|
|
894
1989
|
|
|
895
|
-
|
|
896
1990
|
self.jinja_env = Environment(undefined=SilentUndefined)
|
|
897
1991
|
|
|
898
|
-
|
|
899
1992
|
if db_conn is not None:
|
|
900
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
|
|
901
1999
|
|
|
2000
|
+
summary = breathe(
|
|
2001
|
+
messages=messages[-10:],
|
|
2002
|
+
npc=self.forenpc
|
|
2003
|
+
)
|
|
2004
|
+
characterization = summary.get('output')
|
|
902
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
|
+
|
|
903
2038
|
def _load_from_directory(self):
|
|
904
2039
|
"""Load team from directory"""
|
|
905
2040
|
if not os.path.exists(self.team_path):
|
|
906
2041
|
raise ValueError(f"Team directory not found: {self.team_path}")
|
|
907
2042
|
|
|
908
|
-
# Load team context if available
|
|
909
|
-
|
|
910
2043
|
for filename in os.listdir(self.team_path):
|
|
911
2044
|
if filename.endswith(".npc"):
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
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
|
|
916
2048
|
|
|
917
|
-
except Exception as e:
|
|
918
|
-
print(f"Error loading NPC {filename}: {e}")
|
|
919
2049
|
self.context = self._load_team_context()
|
|
920
|
-
|
|
921
|
-
# Load jinxs from jinxs directory
|
|
2050
|
+
|
|
922
2051
|
jinxs_dir = os.path.join(self.team_path, "jinxs")
|
|
923
2052
|
if os.path.exists(jinxs_dir):
|
|
924
2053
|
for jinx in load_jinxs_from_directory(jinxs_dir):
|
|
925
2054
|
self.jinxs_dict[jinx.jinx_name] = jinx
|
|
926
2055
|
|
|
927
|
-
# Load sub-teams (subfolders)
|
|
928
2056
|
self._load_sub_teams()
|
|
929
2057
|
|
|
930
|
-
|
|
931
|
-
|
|
932
2058
|
def _load_team_context(self):
|
|
933
2059
|
"""Load team context from .ctx file"""
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
#check if any .ctx file exists
|
|
937
2060
|
for fname in os.listdir(self.team_path):
|
|
938
2061
|
if fname.endswith('.ctx'):
|
|
939
|
-
# do stuff on the file
|
|
940
2062
|
ctx_data = load_yaml_file(os.path.join(self.team_path, fname))
|
|
941
2063
|
if ctx_data is not None:
|
|
942
2064
|
if 'model' in ctx_data:
|
|
@@ -964,11 +2086,12 @@ class Team:
|
|
|
964
2086
|
self.databases = ctx_data['databases']
|
|
965
2087
|
else:
|
|
966
2088
|
self.databases = []
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
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
|
|
972
2095
|
if 'preferences' in ctx_data:
|
|
973
2096
|
self.preferences = ctx_data['preferences']
|
|
974
2097
|
else:
|
|
@@ -978,7 +2101,7 @@ class Team:
|
|
|
978
2101
|
else:
|
|
979
2102
|
self.forenpc = self.npcs[list(self.npcs.keys())[0]] if self.npcs else None
|
|
980
2103
|
for key, item in ctx_data.items():
|
|
981
|
-
if key not in ['name', 'mcp_servers', 'databases', 'context']:
|
|
2104
|
+
if key not in ['name', 'mcp_servers', 'databases', 'context', 'file_patterns']:
|
|
982
2105
|
self.shared_context[key] = item
|
|
983
2106
|
return ctx_data
|
|
984
2107
|
return {}
|
|
@@ -991,14 +2114,10 @@ class Team:
|
|
|
991
2114
|
not item.startswith('.') and
|
|
992
2115
|
item != "jinxs"):
|
|
993
2116
|
|
|
994
|
-
# Check if directory contains NPCs
|
|
995
2117
|
if any(f.endswith(".npc") for f in os.listdir(item_path)
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
self.sub_teams[item] = sub_team
|
|
1000
|
-
except Exception as e:
|
|
1001
|
-
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
|
|
1002
2121
|
|
|
1003
2122
|
def get_forenpc(self):
|
|
1004
2123
|
"""
|
|
@@ -1011,9 +2130,7 @@ class Team:
|
|
|
1011
2130
|
if hasattr(self, 'context') and self.context and 'forenpc' in self.context:
|
|
1012
2131
|
forenpc_ref = self.context['forenpc']
|
|
1013
2132
|
|
|
1014
|
-
# Handle Jinja template references
|
|
1015
2133
|
if '{{ref(' in forenpc_ref:
|
|
1016
|
-
# Extract NPC name from {{ref('npc_name')}}
|
|
1017
2134
|
match = re.search(r"{{\s*ref\('([^']+)'\)\s*}}", forenpc_ref)
|
|
1018
2135
|
if match:
|
|
1019
2136
|
forenpc_name = match.group(1)
|
|
@@ -1028,7 +2145,7 @@ class Team:
|
|
|
1028
2145
|
forenpc_api_url=self.context.get('api_url', None)
|
|
1029
2146
|
|
|
1030
2147
|
forenpc = NPC(name='forenpc',
|
|
1031
|
-
|
|
2148
|
+
primary_directive="""You are the forenpc of the team, coordinating activities
|
|
1032
2149
|
between NPCs on the team, verifying that results from
|
|
1033
2150
|
NPCs are high quality and can help to adequately answer
|
|
1034
2151
|
user requests.""",
|
|
@@ -1041,20 +2158,19 @@ class Team:
|
|
|
1041
2158
|
self.npcs[forenpc.name] = forenpc
|
|
1042
2159
|
return forenpc
|
|
1043
2160
|
return None
|
|
2161
|
+
|
|
1044
2162
|
def get_npc(self, npc_ref):
|
|
1045
2163
|
"""Get NPC by name or reference with hierarchical lookup capability"""
|
|
1046
2164
|
if isinstance(npc_ref, NPC):
|
|
1047
2165
|
return npc_ref
|
|
1048
2166
|
elif isinstance(npc_ref, str):
|
|
1049
|
-
# First check direct NPCs
|
|
1050
2167
|
if npc_ref in self.npcs:
|
|
1051
2168
|
return self.npcs[npc_ref]
|
|
1052
2169
|
|
|
1053
|
-
# Then check sub-teams (hierarchical capability)
|
|
1054
2170
|
for sub_team_name, sub_team in self.sub_teams.items():
|
|
1055
2171
|
if npc_ref in sub_team.npcs:
|
|
1056
2172
|
return sub_team.npcs[npc_ref]
|
|
1057
|
-
|
|
2173
|
+
|
|
1058
2174
|
result = sub_team.get_npc(npc_ref)
|
|
1059
2175
|
if result:
|
|
1060
2176
|
return result
|
|
@@ -1069,27 +2185,22 @@ class Team:
|
|
|
1069
2185
|
if not forenpc:
|
|
1070
2186
|
return {"error": "No forenpc available to coordinate the team"}
|
|
1071
2187
|
|
|
1072
|
-
# Log the orchestration start
|
|
1073
2188
|
log_entry(
|
|
1074
2189
|
self.name,
|
|
1075
2190
|
"orchestration_start",
|
|
1076
2191
|
{"request": request}
|
|
1077
2192
|
)
|
|
1078
2193
|
|
|
1079
|
-
# Initial request goes to forenpc
|
|
1080
2194
|
result = forenpc.check_llm_command(request,
|
|
1081
2195
|
context=getattr(self, 'context', {}),
|
|
1082
2196
|
team = self,
|
|
1083
2197
|
)
|
|
1084
2198
|
|
|
1085
|
-
# Track execution until complete
|
|
1086
2199
|
while True:
|
|
1087
|
-
# Save the result
|
|
1088
2200
|
completion_prompt= ""
|
|
1089
2201
|
if isinstance(result, dict):
|
|
1090
2202
|
self.shared_context["execution_history"].append(result)
|
|
1091
2203
|
|
|
1092
|
-
# Track messages by NPC
|
|
1093
2204
|
if result.get("messages") and result.get("npc_name"):
|
|
1094
2205
|
if result["npc_name"] not in self.shared_context["npc_messages"]:
|
|
1095
2206
|
self.shared_context["npc_messages"][result["npc_name"]] = []
|
|
@@ -1134,7 +2245,7 @@ class Team:
|
|
|
1134
2245
|
of misunderstanding, but as long as the response is clearly relevant
|
|
1135
2246
|
to the input request and along the user's intended direction,
|
|
1136
2247
|
it is considered relevant.
|
|
1137
|
-
|
|
2248
|
+
|
|
1138
2249
|
|
|
1139
2250
|
If there is enough information to begin a fruitful conversation with the user,
|
|
1140
2251
|
please consider the request relevant so that we do not
|
|
@@ -1149,7 +2260,7 @@ class Team:
|
|
|
1149
2260
|
-'relevant' with boolean value
|
|
1150
2261
|
-'explanation' for irrelevance with quoted citations in your explanation noting why it is irrelevant to user input must be a single string.
|
|
1151
2262
|
Return only the JSON object."""
|
|
1152
|
-
|
|
2263
|
+
|
|
1153
2264
|
completion_check = npy.llm_funcs.get_llm_response(
|
|
1154
2265
|
completion_prompt,
|
|
1155
2266
|
model=forenpc.model,
|
|
@@ -1159,19 +2270,15 @@ class Team:
|
|
|
1159
2270
|
npc=forenpc,
|
|
1160
2271
|
format="json"
|
|
1161
2272
|
)
|
|
1162
|
-
|
|
2273
|
+
|
|
1163
2274
|
if isinstance(completion_check.get("response"), dict):
|
|
1164
2275
|
complete = completion_check["response"].get("relevant", False)
|
|
1165
2276
|
explanation = completion_check["response"].get("explanation", "")
|
|
1166
2277
|
else:
|
|
1167
|
-
# Default to incomplete if format is wrong
|
|
1168
2278
|
complete = False
|
|
1169
2279
|
explanation = "Could not determine completion status"
|
|
1170
2280
|
|
|
1171
|
-
#import pdb
|
|
1172
|
-
#pdb.set_trace()
|
|
1173
2281
|
if complete:
|
|
1174
|
-
|
|
1175
2282
|
debrief = npy.llm_funcs.get_llm_response(
|
|
1176
2283
|
f"""Context:
|
|
1177
2284
|
Original request: {request}
|
|
@@ -1191,14 +2298,12 @@ class Team:
|
|
|
1191
2298
|
format="json"
|
|
1192
2299
|
)
|
|
1193
2300
|
|
|
1194
|
-
|
|
1195
2301
|
return {
|
|
1196
2302
|
"debrief": debrief.get("response"),
|
|
1197
2303
|
"output": result.get("output"),
|
|
1198
2304
|
"execution_history": self.shared_context["execution_history"],
|
|
1199
2305
|
}
|
|
1200
2306
|
else:
|
|
1201
|
-
# Continue with updated request
|
|
1202
2307
|
updated_request = (
|
|
1203
2308
|
request
|
|
1204
2309
|
+ "\n\nThe request has not yet been fully completed. "
|
|
@@ -1206,9 +2311,7 @@ class Team:
|
|
|
1206
2311
|
+ "\nPlease address only the remaining parts of the request."
|
|
1207
2312
|
)
|
|
1208
2313
|
print('updating request', updated_request)
|
|
1209
|
-
|
|
1210
2314
|
|
|
1211
|
-
# Call forenpc again
|
|
1212
2315
|
result = forenpc.check_llm_command(
|
|
1213
2316
|
updated_request,
|
|
1214
2317
|
context=getattr(self, 'context', {}),
|
|
@@ -1235,542 +2338,101 @@ class Team:
|
|
|
1235
2338
|
if not directory:
|
|
1236
2339
|
raise ValueError("No directory specified for saving team")
|
|
1237
2340
|
|
|
1238
|
-
# Create team directory
|
|
1239
2341
|
ensure_dirs_exist(directory)
|
|
1240
2342
|
|
|
1241
|
-
# Save context
|
|
1242
2343
|
if hasattr(self, 'context') and self.context:
|
|
1243
2344
|
ctx_path = os.path.join(directory, "team.ctx")
|
|
1244
2345
|
write_yaml_file(ctx_path, self.context)
|
|
1245
2346
|
|
|
1246
|
-
# Save NPCs
|
|
1247
2347
|
for npc in self.npcs.values():
|
|
1248
2348
|
npc.save(directory)
|
|
1249
2349
|
|
|
1250
|
-
# Create jinxs directory
|
|
1251
2350
|
jinxs_dir = os.path.join(directory, "jinxs")
|
|
1252
2351
|
ensure_dirs_exist(jinxs_dir)
|
|
1253
2352
|
|
|
1254
|
-
# Save jinxs
|
|
1255
2353
|
for jinx in self.jinxs.values():
|
|
1256
2354
|
jinx.save(jinxs_dir)
|
|
1257
2355
|
|
|
1258
|
-
# Save sub-teams
|
|
1259
2356
|
for team_name, team in self.sub_teams.items():
|
|
1260
2357
|
team_dir = os.path.join(directory, team_name)
|
|
1261
2358
|
team.save(team_dir)
|
|
1262
2359
|
|
|
1263
2360
|
return True
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
self.npc_team = npc_team
|
|
1269
|
-
self.steps = []
|
|
1270
|
-
|
|
1271
|
-
if pipeline_path:
|
|
1272
|
-
self._load_from_path(pipeline_path)
|
|
1273
|
-
elif pipeline_data:
|
|
1274
|
-
self.name = pipeline_data.get("name", "unnamed_pipeline")
|
|
1275
|
-
self.steps = pipeline_data.get("steps", [])
|
|
1276
|
-
else:
|
|
1277
|
-
raise ValueError("Either pipeline_data or pipeline_path must be provided")
|
|
1278
|
-
|
|
1279
|
-
def _load_from_path(self, path):
|
|
1280
|
-
"""Load pipeline from file"""
|
|
1281
|
-
pipeline_data = load_yaml_file(path)
|
|
1282
|
-
if not pipeline_data:
|
|
1283
|
-
raise ValueError(f"Failed to load pipeline from {path}")
|
|
1284
|
-
|
|
1285
|
-
self.name = os.path.splitext(os.path.basename(path))[0]
|
|
1286
|
-
self.steps = pipeline_data.get("steps", [])
|
|
1287
|
-
self.pipeline_path = path
|
|
1288
|
-
|
|
1289
|
-
def execute(self, initial_context=None):
|
|
1290
|
-
"""Execute the pipeline with given context"""
|
|
1291
|
-
context = initial_context or {}
|
|
1292
|
-
results = {}
|
|
1293
|
-
|
|
1294
|
-
# Initialize database tables
|
|
1295
|
-
init_db_tables()
|
|
1296
|
-
|
|
1297
|
-
# Generate pipeline hash for tracking
|
|
1298
|
-
pipeline_hash = self._generate_hash()
|
|
1299
|
-
|
|
1300
|
-
# Create results table specific to this pipeline
|
|
1301
|
-
results_table = f"{self.name}_results"
|
|
1302
|
-
self._ensure_results_table(results_table)
|
|
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 {}
|
|
1303
2365
|
|
|
1304
|
-
|
|
1305
|
-
run_id = self._create_run_entry(pipeline_hash)
|
|
2366
|
+
file_cache = {}
|
|
1306
2367
|
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
"source": self._fetch_data_from_source,
|
|
1311
|
-
})
|
|
1312
|
-
|
|
1313
|
-
# Execute each step
|
|
1314
|
-
for step in self.steps:
|
|
1315
|
-
step_name = step.get("step_name")
|
|
1316
|
-
if not step_name:
|
|
1317
|
-
raise ValueError(f"Missing step_name in step: {step}")
|
|
1318
|
-
|
|
1319
|
-
# Get NPC for this step
|
|
1320
|
-
npc_name = self._render_template(step.get("npc", ""), context)
|
|
1321
|
-
npc = self._get_npc(npc_name)
|
|
1322
|
-
if not npc:
|
|
1323
|
-
raise ValueError(f"NPC {npc_name} not found for step {step_name}")
|
|
1324
|
-
|
|
1325
|
-
# Render task template
|
|
1326
|
-
task = self._render_template(step.get("task", ""), context)
|
|
1327
|
-
|
|
1328
|
-
# Execute with appropriate NPC
|
|
1329
|
-
model = step.get("model", npc.model)
|
|
1330
|
-
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}
|
|
1331
2371
|
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
response = self._execute_mixa_step(step, context, npc, model, provider)
|
|
1336
|
-
else:
|
|
1337
|
-
# Check for data source
|
|
1338
|
-
source_matches = re.findall(r"{{\s*source\('([^']+)'\)\s*}}", task)
|
|
1339
|
-
if source_matches:
|
|
1340
|
-
response = self._execute_data_source_step(step, context, source_matches, npc, model, provider)
|
|
1341
|
-
else:
|
|
1342
|
-
# Standard LLM execution
|
|
1343
|
-
llm_response = npy.llm_funcs.get_llm_response(task, model=model, provider=provider, npc=npc)
|
|
1344
|
-
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", ".")
|
|
1345
2375
|
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
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)
|
|
1349
2382
|
|
|
1350
|
-
|
|
1351
|
-
self._store_step_result(run_id, step_name, npc_name, model, provider,
|
|
1352
|
-
{"task": task}, response, results_table)
|
|
2383
|
+
matching_files = self._find_matching_files(pattern, base_path, recursive)
|
|
1353
2384
|
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
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
|
|
1359
2390
|
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
try:
|
|
1366
|
-
template = Template(template_str)
|
|
1367
|
-
return template.render(**context)
|
|
1368
|
-
except Exception as e:
|
|
1369
|
-
print(f"Error rendering template: {e}")
|
|
1370
|
-
return template_str
|
|
1371
|
-
|
|
1372
|
-
def _get_npc(self, npc_name):
|
|
1373
|
-
"""Get NPC by name from team"""
|
|
1374
|
-
if not self.npc_team:
|
|
1375
|
-
raise ValueError("No NPC team available")
|
|
1376
|
-
|
|
1377
|
-
return self.npc_team.get_npc(npc_name)
|
|
1378
|
-
|
|
1379
|
-
def _generate_hash(self):
|
|
1380
|
-
"""Generate a hash for the pipeline"""
|
|
1381
|
-
if hasattr(self, 'pipeline_path') and self.pipeline_path:
|
|
1382
|
-
with open(self.pipeline_path, 'r') as f:
|
|
1383
|
-
content = f.read()
|
|
1384
|
-
return hashlib.sha256(content.encode()).hexdigest()
|
|
1385
|
-
else:
|
|
1386
|
-
# Generate hash from steps
|
|
1387
|
-
content = json.dumps(self.steps)
|
|
1388
|
-
return hashlib.sha256(content.encode()).hexdigest()
|
|
1389
|
-
|
|
1390
|
-
def _ensure_results_table(self, table_name):
|
|
1391
|
-
"""Ensure results table exists"""
|
|
1392
|
-
db_path = "~/npcsh_history.db"
|
|
1393
|
-
with sqlite3.connect(os.path.expanduser(db_path)) as conn:
|
|
1394
|
-
conn.execute(f"""
|
|
1395
|
-
CREATE TABLE IF NOT EXISTS {table_name} (
|
|
1396
|
-
result_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1397
|
-
run_id INTEGER,
|
|
1398
|
-
step_name TEXT,
|
|
1399
|
-
npc_name TEXT,
|
|
1400
|
-
model TEXT,
|
|
1401
|
-
provider TEXT,
|
|
1402
|
-
inputs TEXT,
|
|
1403
|
-
outputs TEXT,
|
|
1404
|
-
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
1405
|
-
FOREIGN KEY(run_id) REFERENCES pipeline_runs(run_id)
|
|
1406
|
-
)
|
|
1407
|
-
""")
|
|
1408
|
-
conn.commit()
|
|
1409
|
-
|
|
1410
|
-
def _create_run_entry(self, pipeline_hash):
|
|
1411
|
-
"""Create run entry in pipeline_runs table"""
|
|
1412
|
-
db_path = "~/npcsh_history.db"
|
|
1413
|
-
with sqlite3.connect(os.path.expanduser(db_path)) as conn:
|
|
1414
|
-
cursor = conn.execute(
|
|
1415
|
-
"INSERT INTO pipeline_runs (pipeline_name, pipeline_hash, timestamp) VALUES (?, ?, ?)",
|
|
1416
|
-
(self.name, pipeline_hash, datetime.now())
|
|
1417
|
-
)
|
|
1418
|
-
conn.commit()
|
|
1419
|
-
return cursor.lastrowid
|
|
1420
|
-
|
|
1421
|
-
def _store_step_result(self, run_id, step_name, npc_name, model, provider, inputs, outputs, table_name):
|
|
1422
|
-
"""Store step result in database"""
|
|
1423
|
-
db_path = "~/npcsh_history.db"
|
|
1424
|
-
with sqlite3.connect(os.path.expanduser(db_path)) as conn:
|
|
1425
|
-
conn.execute(
|
|
1426
|
-
f"""
|
|
1427
|
-
INSERT INTO {table_name}
|
|
1428
|
-
(run_id, step_name, npc_name, model, provider, inputs, outputs)
|
|
1429
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1430
|
-
""",
|
|
1431
|
-
(
|
|
1432
|
-
run_id,
|
|
1433
|
-
step_name,
|
|
1434
|
-
npc_name,
|
|
1435
|
-
model,
|
|
1436
|
-
provider,
|
|
1437
|
-
json.dumps(self._clean_for_json(inputs)),
|
|
1438
|
-
json.dumps(self._clean_for_json(outputs))
|
|
1439
|
-
)
|
|
1440
|
-
)
|
|
1441
|
-
conn.commit()
|
|
1442
|
-
|
|
1443
|
-
def _clean_for_json(self, obj):
|
|
1444
|
-
"""Clean an object for JSON serialization"""
|
|
1445
|
-
if isinstance(obj, dict):
|
|
1446
|
-
return {
|
|
1447
|
-
k: self._clean_for_json(v)
|
|
1448
|
-
for k, v in obj.items()
|
|
1449
|
-
if not k.startswith("_") and not callable(v)
|
|
1450
|
-
}
|
|
1451
|
-
elif isinstance(obj, list):
|
|
1452
|
-
return [self._clean_for_json(i) for i in obj]
|
|
1453
|
-
elif isinstance(obj, (str, int, float, bool, type(None))):
|
|
1454
|
-
return obj
|
|
1455
|
-
else:
|
|
1456
|
-
return str(obj)
|
|
1457
|
-
|
|
1458
|
-
def _fetch_data_from_source(self, table_name):
|
|
1459
|
-
"""Fetch data from a database table"""
|
|
1460
|
-
db_path = "~/npcsh_history.db"
|
|
1461
|
-
try:
|
|
1462
|
-
engine = create_engine(f"sqlite:///{os.path.expanduser(db_path)}")
|
|
1463
|
-
df = pd.read_sql(f"SELECT * FROM {table_name}", engine)
|
|
1464
|
-
return df.to_json(orient="records")
|
|
1465
|
-
except Exception as e:
|
|
1466
|
-
print(f"Error fetching data from {table_name}: {e}")
|
|
1467
|
-
return "[]"
|
|
1468
|
-
|
|
1469
|
-
def _execute_mixa_step(self, step, context, npc, model, provider):
|
|
1470
|
-
"""Execute a mixture of agents step"""
|
|
1471
|
-
# Get task template
|
|
1472
|
-
task = self._render_template(step.get("task", ""), context)
|
|
1473
|
-
|
|
1474
|
-
# Get configuration
|
|
1475
|
-
mixa_turns = step.get("mixa_turns", 5)
|
|
1476
|
-
num_generating_agents = len(step.get("mixa_agents", []))
|
|
1477
|
-
if num_generating_agents == 0:
|
|
1478
|
-
num_generating_agents = 3 # Default
|
|
1479
|
-
|
|
1480
|
-
num_voting_agents = len(step.get("mixa_voters", []))
|
|
1481
|
-
if num_voting_agents == 0:
|
|
1482
|
-
num_voting_agents = 3 # Default
|
|
1483
|
-
|
|
1484
|
-
# Step 1: Initial Response Generation
|
|
1485
|
-
round_responses = []
|
|
1486
|
-
for _ in range(num_generating_agents):
|
|
1487
|
-
response = npy.llm_funcs.get_llm_response(task, model=model, provider=provider, npc=npc)
|
|
1488
|
-
round_responses.append(response.get("response", ""))
|
|
1489
|
-
|
|
1490
|
-
# Loop for each round of voting and refining
|
|
1491
|
-
for turn in range(1, mixa_turns + 1):
|
|
1492
|
-
# Step 2: Voting by agents
|
|
1493
|
-
votes = [0] * len(round_responses)
|
|
1494
|
-
for _ in range(num_voting_agents):
|
|
1495
|
-
voted_index = random.choice(range(len(round_responses)))
|
|
1496
|
-
votes[voted_index] += 1
|
|
1497
|
-
|
|
1498
|
-
# Step 3: Refinement feedback
|
|
1499
|
-
refined_responses = []
|
|
1500
|
-
for i, resp in enumerate(round_responses):
|
|
1501
|
-
feedback = (
|
|
1502
|
-
f"Current responses and their votes:\n" +
|
|
1503
|
-
"\n".join([f"Response {j+1}: {r[:100]}... - Votes: {votes[j]}"
|
|
1504
|
-
for j, r in enumerate(round_responses)]) +
|
|
1505
|
-
f"\n\nRefine your response #{i+1}: {resp}"
|
|
1506
|
-
)
|
|
1507
|
-
|
|
1508
|
-
response = npy.llm_funcs.get_llm_response(feedback, model=model, provider=provider, npc=npc)
|
|
1509
|
-
refined_responses.append(response.get("response", ""))
|
|
1510
|
-
|
|
1511
|
-
# Update responses for next round
|
|
1512
|
-
round_responses = refined_responses
|
|
1513
|
-
|
|
1514
|
-
# Final synthesis
|
|
1515
|
-
synthesis_prompt = (
|
|
1516
|
-
"Synthesize these responses into a coherent answer:\n" +
|
|
1517
|
-
"\n".join(round_responses)
|
|
1518
|
-
)
|
|
1519
|
-
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 = []
|
|
1520
2396
|
|
|
1521
|
-
|
|
2397
|
+
if not os.path.exists(base_path):
|
|
2398
|
+
return matching_files
|
|
1522
2399
|
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
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}")
|
|
1527
2413
|
|
|
2414
|
+
return matching_files
|
|
2415
|
+
|
|
2416
|
+
def _load_file_content(self, file_path):
|
|
2417
|
+
"""Load content from a file with error handling"""
|
|
1528
2418
|
try:
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
engine = create_engine(f"sqlite:///{os.path.expanduser(db_path)}")
|
|
1532
|
-
df = pd.read_sql(f"SELECT * FROM {table_name}", engine)
|
|
1533
|
-
|
|
1534
|
-
# Handle batch mode vs. individual processing
|
|
1535
|
-
if step.get("batch_mode", False):
|
|
1536
|
-
# Replace source reference with all data
|
|
1537
|
-
data_str = df.to_json(orient="records")
|
|
1538
|
-
task = task_template.replace(f"{{{{ source('{table_name}') }}}}", data_str)
|
|
1539
|
-
task = self._render_template(task, context)
|
|
1540
|
-
|
|
1541
|
-
# Process all at once
|
|
1542
|
-
response = npy.llm_funcs.get_llm_response(task, model=model, provider=provider, npc=npc)
|
|
1543
|
-
return response.get("response", "")
|
|
1544
|
-
else:
|
|
1545
|
-
# Process each row individually
|
|
1546
|
-
results = []
|
|
1547
|
-
for _, row in df.iterrows():
|
|
1548
|
-
# Replace source reference with row data
|
|
1549
|
-
row_data = json.dumps(row.to_dict())
|
|
1550
|
-
row_task = task_template.replace(f"{{{{ source('{table_name}') }}}}", row_data)
|
|
1551
|
-
row_task = self._render_template(row_task, context)
|
|
1552
|
-
|
|
1553
|
-
# Process row
|
|
1554
|
-
response = npy.llm_funcs.get_llm_response(row_task, model=model, provider=provider, npc=npc)
|
|
1555
|
-
results.append(response.get("response", ""))
|
|
1556
|
-
|
|
1557
|
-
return results
|
|
2419
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
2420
|
+
return f.read()
|
|
1558
2421
|
except Exception as e:
|
|
1559
|
-
print(f"Error
|
|
1560
|
-
return
|
|
1561
|
-
|
|
1562
|
-
|
|
2422
|
+
print(f"Error reading {file_path}: {e}")
|
|
2423
|
+
return None
|
|
1563
2424
|
|
|
1564
|
-
def log_entry(entity_id, entry_type, content, metadata=None, db_path="~/npcsh_history.db"):
|
|
1565
|
-
"""Log an entry for an NPC or team"""
|
|
1566
|
-
db_path = os.path.expanduser(db_path)
|
|
1567
|
-
with sqlite3.connect(db_path) as conn:
|
|
1568
|
-
conn.execute(
|
|
1569
|
-
"INSERT INTO npc_log (entity_id, entry_type, content, metadata) VALUES (?, ?, ?, ?)",
|
|
1570
|
-
(entity_id, entry_type, json.dumps(content), json.dumps(metadata) if metadata else None)
|
|
1571
|
-
)
|
|
1572
|
-
conn.commit()
|
|
1573
2425
|
|
|
1574
|
-
def
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
query = "SELECT entry_type, content, metadata, timestamp FROM npc_log WHERE entity_id = ?"
|
|
1579
|
-
params = [entity_id]
|
|
1580
|
-
|
|
1581
|
-
if entry_type:
|
|
1582
|
-
query += " AND entry_type = ?"
|
|
1583
|
-
params.append(entry_type)
|
|
1584
|
-
|
|
1585
|
-
query += " ORDER BY timestamp DESC LIMIT ?"
|
|
1586
|
-
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 ""
|
|
1587
2430
|
|
|
1588
|
-
|
|
2431
|
+
context_parts = ["Additional context from files:"]
|
|
1589
2432
|
|
|
1590
|
-
|
|
1591
|
-
{
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
"metadata": json.loads(r[2]) if r[2] else None,
|
|
1595
|
-
"timestamp": r[3]
|
|
1596
|
-
}
|
|
1597
|
-
for r in results
|
|
1598
|
-
]
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
def load_yaml_file(file_path):
|
|
1602
|
-
"""Load a YAML file with error handling"""
|
|
1603
|
-
try:
|
|
1604
|
-
with open(os.path.expanduser(file_path), 'r') as f:
|
|
1605
|
-
return yaml.safe_load(f)
|
|
1606
|
-
except Exception as e:
|
|
1607
|
-
print(f"Error loading YAML file {file_path}: {e}")
|
|
1608
|
-
return None
|
|
1609
|
-
|
|
1610
|
-
def write_yaml_file(file_path, data):
|
|
1611
|
-
"""Write data to a YAML file"""
|
|
1612
|
-
try:
|
|
1613
|
-
with open(os.path.expanduser(file_path), 'w') as f:
|
|
1614
|
-
yaml.dump(data, f)
|
|
1615
|
-
return True
|
|
1616
|
-
except Exception as e:
|
|
1617
|
-
print(f"Error writing YAML file {file_path}: {e}")
|
|
1618
|
-
return False
|
|
1619
|
-
|
|
1620
|
-
def create_or_replace_table(db_path, table_name, data):
|
|
1621
|
-
"""Creates or replaces a table in the SQLite database"""
|
|
1622
|
-
conn = sqlite3.connect(os.path.expanduser(db_path))
|
|
1623
|
-
try:
|
|
1624
|
-
data.to_sql(table_name, conn, if_exists="replace", index=False)
|
|
1625
|
-
print(f"Table '{table_name}' created/replaced successfully.")
|
|
1626
|
-
return True
|
|
1627
|
-
except Exception as e:
|
|
1628
|
-
print(f"Error creating/replacing table '{table_name}': {e}")
|
|
1629
|
-
return False
|
|
1630
|
-
finally:
|
|
1631
|
-
conn.close()
|
|
1632
|
-
|
|
1633
|
-
def find_file_path(filename, search_dirs, suffix=None):
|
|
1634
|
-
"""Find a file in multiple directories"""
|
|
1635
|
-
if suffix and not filename.endswith(suffix):
|
|
1636
|
-
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("")
|
|
1637
2437
|
|
|
1638
|
-
|
|
1639
|
-
file_path = os.path.join(os.path.expanduser(dir_path), filename)
|
|
1640
|
-
if os.path.exists(file_path):
|
|
1641
|
-
return file_path
|
|
1642
|
-
|
|
1643
|
-
return None
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
def initialize_npc_project(
|
|
1648
|
-
directory=None,
|
|
1649
|
-
templates=None,
|
|
1650
|
-
context=None,
|
|
1651
|
-
model=None,
|
|
1652
|
-
provider=None,
|
|
1653
|
-
) -> str:
|
|
1654
|
-
"""Initialize an NPC project"""
|
|
1655
|
-
if directory is None:
|
|
1656
|
-
directory = os.getcwd()
|
|
1657
|
-
|
|
1658
|
-
npc_team_dir = os.path.join(directory, "npc_team")
|
|
1659
|
-
os.makedirs(npc_team_dir, exist_ok=True)
|
|
1660
|
-
|
|
1661
|
-
for subdir in ["jinxs",
|
|
1662
|
-
"assembly_lines",
|
|
1663
|
-
"sql_models",
|
|
1664
|
-
"jobs",
|
|
1665
|
-
"triggers"]:
|
|
1666
|
-
os.makedirs(os.path.join(npc_team_dir, subdir), exist_ok=True)
|
|
1667
|
-
|
|
1668
|
-
forenpc_path = os.path.join(npc_team_dir, "forenpc.npc")
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
# Always ensure default NPC exists
|
|
1672
|
-
if not os.path.exists(forenpc_path):
|
|
1673
|
-
# Use your new NPC class to create sibiji
|
|
1674
|
-
default_npc = {
|
|
1675
|
-
"name": "forenpc",
|
|
1676
|
-
"primary_directive": "You are the forenpc of an NPC team",
|
|
1677
|
-
}
|
|
1678
|
-
with open(forenpc_path, "w") as f:
|
|
1679
|
-
yaml.dump(default_npc, f)
|
|
1680
|
-
ctx_path = os.path.join(npc_team_dir, "team.ctx")
|
|
1681
|
-
if not os.path.exists(ctx_path):
|
|
1682
|
-
default_ctx = {
|
|
1683
|
-
'name': '',
|
|
1684
|
-
'context' : '',
|
|
1685
|
-
'preferences': '',
|
|
1686
|
-
'mcp_servers': '',
|
|
1687
|
-
'databases':'',
|
|
1688
|
-
'use_global_jinxs': True,
|
|
1689
|
-
'forenpc': 'forenpc'
|
|
1690
|
-
}
|
|
1691
|
-
with open(ctx_path, "w") as f:
|
|
1692
|
-
yaml.dump(default_ctx, f)
|
|
1693
|
-
|
|
1694
|
-
return f"NPC project initialized in {npc_team_dir}"
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
def execute_jinx_command(
|
|
1701
|
-
jinx: Jinx,
|
|
1702
|
-
args: List[str],
|
|
1703
|
-
messages=None,
|
|
1704
|
-
npc: NPC = None,
|
|
1705
|
-
) -> Dict[str, Any]:
|
|
1706
|
-
"""
|
|
1707
|
-
Execute a jinx command with the given arguments.
|
|
1708
|
-
"""
|
|
1709
|
-
# Extract inputs for the current jinx
|
|
1710
|
-
input_values = extract_jinx_inputs(args, jinx)
|
|
1711
|
-
|
|
1712
|
-
# print(f"Input values: {input_values}")
|
|
1713
|
-
# Execute the jinx with the extracted inputs
|
|
1714
|
-
|
|
1715
|
-
jinx_output = jinx.execute(
|
|
1716
|
-
input_values,
|
|
1717
|
-
jinx.jinx_name,
|
|
1718
|
-
npc=npc,
|
|
1719
|
-
)
|
|
1720
|
-
|
|
1721
|
-
return {"messages": messages, "output": jinx_output}
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
def extract_jinx_inputs(args: List[str], jinx: Jinx) -> Dict[str, Any]:
|
|
1726
|
-
inputs = {}
|
|
1727
|
-
|
|
1728
|
-
# Create flag mapping
|
|
1729
|
-
flag_mapping = {}
|
|
1730
|
-
for input_ in jinx.inputs:
|
|
1731
|
-
if isinstance(input_, str):
|
|
1732
|
-
flag_mapping[f"-{input_[0]}"] = input_
|
|
1733
|
-
flag_mapping[f"--{input_}"] = input_
|
|
1734
|
-
elif isinstance(input_, dict):
|
|
1735
|
-
key = list(input_.keys())[0]
|
|
1736
|
-
flag_mapping[f"-{key[0]}"] = key
|
|
1737
|
-
flag_mapping[f"--{key}"] = key
|
|
1738
|
-
|
|
1739
|
-
# Process arguments
|
|
1740
|
-
used_args = set()
|
|
1741
|
-
for i, arg in enumerate(args):
|
|
1742
|
-
if arg in flag_mapping:
|
|
1743
|
-
# If flag is found, next argument is its value
|
|
1744
|
-
if i + 1 < len(args):
|
|
1745
|
-
input_name = flag_mapping[arg]
|
|
1746
|
-
inputs[input_name] = args[i + 1]
|
|
1747
|
-
used_args.add(i)
|
|
1748
|
-
used_args.add(i + 1)
|
|
1749
|
-
else:
|
|
1750
|
-
print(f"Warning: {arg} flag is missing a value.")
|
|
1751
|
-
|
|
1752
|
-
# If no flags used, combine remaining args for first input
|
|
1753
|
-
unused_args = [arg for i, arg in enumerate(args) if i not in used_args]
|
|
1754
|
-
if unused_args and jinx.inputs:
|
|
1755
|
-
first_input = jinx.inputs[0]
|
|
1756
|
-
if isinstance(first_input, str):
|
|
1757
|
-
inputs[first_input] = " ".join(unused_args)
|
|
1758
|
-
elif isinstance(first_input, dict):
|
|
1759
|
-
key = list(first_input.keys())[0]
|
|
1760
|
-
inputs[key] = " ".join(unused_args)
|
|
1761
|
-
|
|
1762
|
-
# Add default values for inputs not provided
|
|
1763
|
-
for input_ in jinx.inputs:
|
|
1764
|
-
if isinstance(input_, str):
|
|
1765
|
-
if input_ not in inputs:
|
|
1766
|
-
if any(args): # If we have any arguments at all
|
|
1767
|
-
raise ValueError(f"Missing required input: {input_}")
|
|
1768
|
-
else:
|
|
1769
|
-
inputs[input_] = None # Allow None for completely empty calls
|
|
1770
|
-
elif isinstance(input_, dict):
|
|
1771
|
-
key = list(input_.keys())[0]
|
|
1772
|
-
if key not in inputs:
|
|
1773
|
-
inputs[key] = input_[key]
|
|
1774
|
-
|
|
1775
|
-
return inputs
|
|
1776
|
-
|
|
2438
|
+
return "\n".join(context_parts)
|