npcpy 1.0.26__py3-none-any.whl → 1.2.32__py3-none-any.whl

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