npcpy 1.2.36__py3-none-any.whl → 1.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
npcpy/npc_sysenv.py CHANGED
@@ -842,8 +842,23 @@ The current date and time are : {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
842
842
 
843
843
  if team is not None:
844
844
  team_context = team.context if hasattr(team, "context") and team.context else ""
845
- team_preferences = team.preferences if hasattr(team, "preferences") and len(team.preferences) > 0 else ""
846
- system_message += f"\nTeam context: {team_context}\nTeam preferences: {team_preferences}\n"
845
+ # preferences now comes from shared_context like other generic context keys
846
+ team_preferences = team.shared_context.get('preferences', '') if hasattr(team, "shared_context") else ""
847
+ system_message += f"\nTeam context: {team_context}\n"
848
+ if team_preferences:
849
+ system_message += f"Team preferences: {team_preferences}\n"
850
+
851
+ # Add team members
852
+ if hasattr(team, 'npcs') and team.npcs:
853
+ members = []
854
+ for name, member in team.npcs.items():
855
+ if name != npc.name: # Don't list self
856
+ directive = getattr(member, 'primary_directive', '')
857
+ # Get first line or first 100 chars
858
+ desc = directive.split('\n')[0][:100] if directive else ''
859
+ members.append(f" - {name}: {desc}")
860
+ if members:
861
+ system_message += "\nTeam members (use delegate tool to assign tasks):\n" + "\n".join(members) + "\n"
847
862
 
848
863
  system_message += """
849
864
  IMPORTANT:
npcpy/serve.py CHANGED
@@ -50,7 +50,6 @@ from npcpy.memory.search import execute_rag_command, execute_brainblast_command
50
50
  from npcpy.data.load import load_file_contents
51
51
  from npcpy.data.web import search_web
52
52
 
53
- from npcsh._state import get_relevant_memories, search_kg_facts
54
53
 
55
54
  import base64
56
55
  import shutil
@@ -67,7 +66,7 @@ from npcpy.memory.command_history import (
67
66
  save_conversation_message,
68
67
  generate_message_id,
69
68
  )
70
- from npcpy.npc_compiler import Jinx, NPC, Team, load_jinxs_from_directory, build_jinx_tool_catalog
69
+ from npcpy.npc_compiler import Jinx, NPC, Team, load_jinxs_from_directory, build_jinx_tool_catalog, initialize_npc_project
71
70
 
72
71
  from npcpy.llm_funcs import (
73
72
  get_llm_response, check_llm_command
@@ -972,12 +971,8 @@ def execute_jinx():
972
971
  'state': state,
973
972
  'CommandHistory': CommandHistory,
974
973
  'load_kg_from_db': load_kg_from_db,
975
- 'execute_rag_command': execute_rag_command,
976
- 'execute_brainblast_command': execute_brainblast_command,
977
- 'load_file_contents': load_file_contents,
978
- 'search_web': search_web,
979
- 'get_relevant_memories': get_relevant_memories,
980
- 'search_kg_facts': search_kg_facts,
974
+ #'get_relevant_memories': get_relevant_memories,
975
+ #'search_kg_facts': search_kg_facts,
981
976
  }
982
977
 
983
978
  jinx_execution_result = jinx.execute(
@@ -1921,6 +1916,134 @@ def get_jinxs_project():
1921
1916
  print(jinx_data)
1922
1917
  return jsonify({"jinxs": jinx_data, "error": None})
1923
1918
 
1919
+ # ============== SQL Models (npcsql) API Endpoints ==============
1920
+ @app.route("/api/npcsql/run_model", methods=["POST"])
1921
+ def run_npcsql_model():
1922
+ """Execute a single SQL model using ModelCompiler"""
1923
+ try:
1924
+ from npcpy.sql.npcsql import ModelCompiler
1925
+
1926
+ data = request.json
1927
+ models_dir = data.get("modelsDir")
1928
+ model_name = data.get("modelName")
1929
+ npc_directory = data.get("npcDirectory", os.path.expanduser("~/.npcsh/npc_team"))
1930
+ target_db = data.get("targetDb", os.path.expanduser("~/npcsh_history.db"))
1931
+
1932
+ if not models_dir or not model_name:
1933
+ return jsonify({"success": False, "error": "modelsDir and modelName are required"}), 400
1934
+
1935
+ if not os.path.exists(models_dir):
1936
+ return jsonify({"success": False, "error": f"Models directory not found: {models_dir}"}), 404
1937
+
1938
+ compiler = ModelCompiler(
1939
+ models_dir=models_dir,
1940
+ target_engine=target_db,
1941
+ npc_directory=npc_directory
1942
+ )
1943
+
1944
+ compiler.discover_models()
1945
+
1946
+ if model_name not in compiler.models:
1947
+ available = list(compiler.models.keys())
1948
+ return jsonify({
1949
+ "success": False,
1950
+ "error": f"Model '{model_name}' not found. Available: {available}"
1951
+ }), 404
1952
+
1953
+ result_df = compiler.execute_model(model_name)
1954
+ row_count = len(result_df) if result_df is not None else 0
1955
+
1956
+ return jsonify({
1957
+ "success": True,
1958
+ "rows": row_count,
1959
+ "message": f"Model '{model_name}' executed successfully. {row_count} rows materialized."
1960
+ })
1961
+
1962
+ except Exception as e:
1963
+ import traceback
1964
+ traceback.print_exc()
1965
+ return jsonify({"success": False, "error": str(e)}), 500
1966
+
1967
+ @app.route("/api/npcsql/run_all", methods=["POST"])
1968
+ def run_all_npcsql_models():
1969
+ """Execute all SQL models in dependency order using ModelCompiler"""
1970
+ try:
1971
+ from npcpy.sql.npcsql import ModelCompiler
1972
+
1973
+ data = request.json
1974
+ models_dir = data.get("modelsDir")
1975
+ npc_directory = data.get("npcDirectory", os.path.expanduser("~/.npcsh/npc_team"))
1976
+ target_db = data.get("targetDb", os.path.expanduser("~/npcsh_history.db"))
1977
+
1978
+ if not models_dir:
1979
+ return jsonify({"success": False, "error": "modelsDir is required"}), 400
1980
+
1981
+ if not os.path.exists(models_dir):
1982
+ return jsonify({"success": False, "error": f"Models directory not found: {models_dir}"}), 404
1983
+
1984
+ compiler = ModelCompiler(
1985
+ models_dir=models_dir,
1986
+ target_engine=target_db,
1987
+ npc_directory=npc_directory
1988
+ )
1989
+
1990
+ results = compiler.run_all_models()
1991
+
1992
+ summary = {
1993
+ name: len(df) if df is not None else 0
1994
+ for name, df in results.items()
1995
+ }
1996
+
1997
+ return jsonify({
1998
+ "success": True,
1999
+ "models_executed": list(results.keys()),
2000
+ "row_counts": summary,
2001
+ "message": f"Executed {len(results)} models successfully."
2002
+ })
2003
+
2004
+ except Exception as e:
2005
+ import traceback
2006
+ traceback.print_exc()
2007
+ return jsonify({"success": False, "error": str(e)}), 500
2008
+
2009
+ @app.route("/api/npcsql/models", methods=["GET"])
2010
+ def list_npcsql_models():
2011
+ """List available SQL models in a directory"""
2012
+ try:
2013
+ from npcpy.sql.npcsql import ModelCompiler
2014
+
2015
+ models_dir = request.args.get("modelsDir")
2016
+ if not models_dir:
2017
+ return jsonify({"success": False, "error": "modelsDir query param required"}), 400
2018
+
2019
+ if not os.path.exists(models_dir):
2020
+ return jsonify({"models": [], "error": None})
2021
+
2022
+ compiler = ModelCompiler(
2023
+ models_dir=models_dir,
2024
+ target_engine=os.path.expanduser("~/npcsh_history.db"),
2025
+ npc_directory=os.path.expanduser("~/.npcsh/npc_team")
2026
+ )
2027
+
2028
+ compiler.discover_models()
2029
+
2030
+ models_info = []
2031
+ for name, model in compiler.models.items():
2032
+ models_info.append({
2033
+ "name": name,
2034
+ "path": model.path,
2035
+ "has_ai_function": model.has_ai_function,
2036
+ "dependencies": list(model.dependencies),
2037
+ "config": model.config
2038
+ })
2039
+
2040
+ return jsonify({"models": models_info, "error": None})
2041
+
2042
+ except Exception as e:
2043
+ import traceback
2044
+ traceback.print_exc()
2045
+ return jsonify({"models": [], "error": str(e)}), 500
2046
+
1924
2047
  @app.route("/api/npc_team_global")
1925
2048
  def get_npc_team_global():
1926
2049
  global_npc_directory = os.path.expanduser("~/.npcsh/npc_team")
@@ -2050,19 +2173,27 @@ def api_get_last_used_in_conversation():
2050
2173
  result = get_last_used_model_and_npc_in_conversation(conversation_id)
2051
2174
  return jsonify(result)
2052
2175
 
2053
- def get_ctx_path(is_global, current_path=None):
2176
+ def get_ctx_path(is_global, current_path=None, create_default=False):
2054
2177
  """Determines the path to the .ctx file."""
2055
2178
  if is_global:
2056
2179
  ctx_dir = os.path.join(os.path.expanduser("~/.npcsh/npc_team/"))
2057
2180
  ctx_files = glob.glob(os.path.join(ctx_dir, "*.ctx"))
2058
- return ctx_files[0] if ctx_files else None
2181
+ if ctx_files:
2182
+ return ctx_files[0]
2183
+ elif create_default:
2184
+ return os.path.join(ctx_dir, "team.ctx")
2185
+ return None
2059
2186
  else:
2060
2187
  if not current_path:
2061
2188
  return None
2062
-
2189
+
2063
2190
  ctx_dir = os.path.join(current_path, "npc_team")
2064
2191
  ctx_files = glob.glob(os.path.join(ctx_dir, "*.ctx"))
2065
- return ctx_files[0] if ctx_files else None
2192
+ if ctx_files:
2193
+ return ctx_files[0]
2194
+ elif create_default:
2195
+ return os.path.join(ctx_dir, "team.ctx")
2196
+ return None
2066
2197
 
2067
2198
 
2068
2199
  def read_ctx_file(file_path):
@@ -2163,10 +2294,10 @@ def save_project_context():
2163
2294
  data = request.json
2164
2295
  current_path = data.get("path")
2165
2296
  context_data = data.get("context", {})
2166
-
2297
+
2167
2298
  if not current_path:
2168
2299
  return jsonify({"error": "Project path is required."}), 400
2169
-
2300
+
2170
2301
  ctx_path = get_ctx_path(is_global=False, current_path=current_path)
2171
2302
  if write_ctx_file(ctx_path, context_data):
2172
2303
  return jsonify({"message": "Project context saved.", "error": None})
@@ -2176,6 +2307,23 @@ def save_project_context():
2176
2307
  print(f"Error saving project context: {e}")
2177
2308
  return jsonify({"error": str(e)}), 500
2178
2309
 
2310
+ @app.route("/api/context/project/init", methods=["POST"])
2311
+ def init_project_team():
2312
+ """Initialize a new npc_team folder in the project directory."""
2313
+ try:
2314
+ data = request.json
2315
+ project_path = data.get("path")
2316
+
2317
+ if not project_path:
2318
+ return jsonify({"error": "Project path is required."}), 400
2319
+
2320
+ # Use the existing initialize_npc_project function
2321
+ result = initialize_npc_project(directory=project_path)
2322
+ return jsonify({"message": "Project team initialized.", "path": result, "error": None})
2323
+ except Exception as e:
2324
+ print(f"Error initializing project team: {e}")
2325
+ return jsonify({"error": str(e)}), 500
2326
+
2179
2327
 
2180
2328
 
2181
2329
 
npcpy/sql/npcsql.py CHANGED
@@ -253,56 +253,81 @@ class NPCSQLOperations:
253
253
  return None
254
254
 
255
255
  def execute_ai_function(
256
- self,
257
- func_name: str,
258
- df: pd.DataFrame,
256
+ self,
257
+ func_name: str,
258
+ df: pd.DataFrame,
259
259
  **params
260
260
  ) -> pd.Series:
261
261
  if func_name not in self.function_map:
262
262
  raise ValueError(f"Unknown AI function: {func_name}")
263
-
263
+
264
264
  func = self.function_map[func_name]
265
-
265
+
266
266
  npc_ref = params.get('npc', '')
267
267
  resolved_npc = self._resolve_npc_reference(npc_ref)
268
-
268
+
269
269
  resolved_team = self._get_team()
270
270
  if not resolved_team and hasattr(resolved_npc, 'team'):
271
271
  resolved_team = resolved_npc.team
272
-
273
- def apply_function_to_row(row):
272
+
273
+ total_rows = len(df)
274
+ print(f"NQL: Executing {func_name} on {total_rows} rows with NPC '{npc_ref}'...")
275
+
276
+ results = []
277
+ for idx, (row_idx, row) in enumerate(df.iterrows()):
274
278
  query_template = params.get('query', '')
275
279
  column_name = params.get('column', '')
276
-
280
+
277
281
  column_value = str(row[column_name]) if column_name and column_name in row.index else column_name
278
282
 
279
283
  if query_template:
280
284
  row_data = {
281
- col: str(row[col])
285
+ col: str(row[col])
282
286
  for col in df.columns
283
287
  }
284
- row_data['column_value'] = column_value
288
+ row_data['column_value'] = column_value
285
289
  query = query_template.format(**row_data)
286
290
  else:
287
291
  query = column_value
288
-
292
+
293
+ print(f" [{idx+1}/{total_rows}] Processing row {row_idx}...", end=" ", flush=True)
294
+
289
295
  sig = py_inspect.signature(func)
296
+
297
+ # Extract model/provider from NPC if available
298
+ npc_model = None
299
+ npc_provider = None
300
+ if resolved_npc and hasattr(resolved_npc, 'model'):
301
+ npc_model = resolved_npc.model
302
+ if resolved_npc and hasattr(resolved_npc, 'provider'):
303
+ npc_provider = resolved_npc.provider
304
+
290
305
  func_params = {
291
306
  k: v for k, v in {
292
- 'prompt': query,
293
- 'text': query,
307
+ 'prompt': query,
308
+ 'text': query,
294
309
  'npc': resolved_npc,
295
310
  'team': resolved_team,
296
- 'context': params.get('context', '')
311
+ 'context': params.get('context', ''),
312
+ 'model': npc_model or 'gpt-4o-mini',
313
+ 'provider': npc_provider or 'openai'
297
314
  }.items() if k in sig.parameters
298
315
  }
299
-
300
- result = func(**func_params)
301
- return (result.get("response", "")
302
- if isinstance(result, dict)
303
- else str(result))
304
-
305
- return df.apply(apply_function_to_row, axis=1)
316
+
317
+ try:
318
+ result = func(**func_params)
319
+ result_value = (result.get("response", "")
320
+ if isinstance(result, dict)
321
+ else str(result))
322
+ print(f"OK ({len(result_value)} chars)")
323
+ except Exception as e:
324
+ print(f"ERROR: {e}")
325
+ result_value = None
326
+
327
+ results.append(result_value)
328
+
329
+ print(f"NQL: Completed {func_name} on {total_rows} rows.")
330
+ return pd.Series(results, index=df.index)
306
331
 
307
332
 
308
333
  # --- SQL Model Definition ---
@@ -360,11 +385,10 @@ class SQLModel:
360
385
  def _extract_ai_functions(self) -> Dict[str, Dict]:
361
386
  """Extract AI function calls from SQL content with improved robustness."""
362
387
  import types
363
-
388
+
364
389
  ai_functions = {}
365
- # More robust pattern that handles nested parentheses better
366
- # This captures: nql.function_name(args...)
367
- pattern = r"nql\.(\w+)\s*\(((?:[^()]|\([^()]*\))*)\)"
390
+ # Pattern that captures: nql.function_name(args...) as alias
391
+ pattern = r"nql\.(\w+)\s*\(((?:[^()]|\([^()]*\))*)\)(\s+as\s+(\w+))?"
368
392
 
369
393
  matches = re.finditer(pattern, self.content, flags=re.DOTALL | re.IGNORECASE)
370
394
 
@@ -424,13 +448,17 @@ class SQLModel:
424
448
  if self.npc_directory and npc_param.startswith(self.npc_directory):
425
449
  npc_param = npc_param[len(self.npc_directory):].strip('/')
426
450
 
451
+ # Extract alias if present (group 4 from the pattern)
452
+ alias = match.group(4) if match.lastindex >= 4 and match.group(4) else f"{func_name}_result"
453
+
427
454
  ai_functions[func_name] = {
428
455
  "column": column_param,
429
456
  "npc": npc_param,
430
457
  "query": query_param,
431
458
  "context": context_param,
432
459
  "full_call_string": full_call_string,
433
- "original_func_name": match.group(1) # Store original case
460
+ "original_func_name": match.group(1), # Store original case
461
+ "alias": alias
434
462
  }
435
463
  else:
436
464
  print(f"DEBUG SQLModel: Function '{func_name}' not found in available LLM funcs ({available_functions}). Skipping this NQL call.")
@@ -546,14 +574,23 @@ class ModelCompiler:
546
574
 
547
575
  def replace_ref(match):
548
576
  model_name = match.group(1)
549
- if model_name not in self.models:
550
- raise ValueError(
551
- f"Model '{model_name}' referenced by '{{{{ ref('{model_name}') }}}}' not found during compilation."
552
- )
553
-
554
- if self.target_schema:
555
- return f"{self.target_schema}.{model_name}"
556
- return model_name
577
+
578
+ # First check if it's a model we're compiling
579
+ if model_name in self.models:
580
+ if self.target_schema:
581
+ return f"{self.target_schema}.{model_name}"
582
+ return model_name
583
+
584
+ # Otherwise, check if it's an existing table in the database
585
+ if self._table_exists(model_name):
586
+ if self.target_schema:
587
+ return f"{self.target_schema}.{model_name}"
588
+ return model_name
589
+
590
+ # If neither, raise an error
591
+ raise ValueError(
592
+ f"Model or table '{model_name}' referenced by '{{{{ ref('{model_name}') }}}}' not found during compilation."
593
+ )
557
594
 
558
595
  replaced_sql = re.sub(ref_pattern, replace_ref, sql_content)
559
596
  return replaced_sql
@@ -665,42 +702,42 @@ class ModelCompiler:
665
702
  for func_name, params in model.ai_functions.items():
666
703
  try:
667
704
  result_series = self.npc_operations.execute_ai_function(func_name, df, **params)
668
- result_column_name = f"{func_name}_{params.get('column', 'result')}" # Use a more specific alias if possible
705
+ # Use the SQL alias if available, otherwise generate one
706
+ result_column_name = params.get('alias', f"{func_name}_result")
669
707
  df[result_column_name] = result_series
670
- print(f"DEBUG: Python-driven AI function '{func_name}' executed. Result in column '{result_column_name}'.")
708
+ print(f"DEBUG: AI function '{func_name}' result stored in column '{result_column_name}'.")
671
709
  except Exception as e:
672
- print(f"ERROR: Executing Python-driven AI function '{func_name}': {e}. Assigning NULL.")
673
- df[f"{func_name}_{params.get('column', 'result')}"] = None
710
+ print(f"ERROR: Executing AI function '{func_name}': {e}. Assigning NULL.")
711
+ result_column_name = params.get('alias', f"{func_name}_result")
712
+ df[result_column_name] = None
674
713
 
675
714
  return df
676
715
 
677
716
  def _replace_nql_calls_with_null(self, sql_content: str, model: SQLModel) -> str:
678
717
  """
679
- Replaces specific nql.func(...) as alias calls with NULL as alias.
680
- This is used for the fallback path or to clean up any NQL calls missed by native translation.
718
+ Replaces nql.func(...) calls with NULL placeholders.
719
+ This is used for the fallback path where we execute SQL first, then apply AI functions in Python.
681
720
  """
682
721
  modified_sql = sql_content
683
- for func_name, params in model.ai_functions.items():
684
- original_nql_call = params.get('full_call_string')
685
- if not original_nql_call:
686
- print(f"WARNING: 'full_call_string' not found for NQL function '{func_name}'. Cannot replace with NULL.")
687
- continue
688
722
 
689
- # Extract alias from the original_nql_call string for NULL replacement
690
- alias_match = re.search(r'\s+as\s+(\w+)(?:\W|$)', original_nql_call, re.IGNORECASE)
691
- alias_name = alias_match.group(1) if alias_match else f"{func_name}_{params.get('column', 'result')}"
723
+ # Pattern to match nql.function_name(...) with nested parentheses support
724
+ # Also captures the 'as alias' part if present
725
+ nql_pattern = r'nql\.(\w+)\s*\(((?:[^()]|\([^()]*\))*)\)(\s+as\s+(\w+))?'
692
726
 
693
- # Create a robust pattern for the original NQL call to handle whitespace variability
694
- escaped_original_call = re.escape(original_nql_call.strip())
695
- pattern_to_sub = re.compile(r"\s*".join(escaped_original_call.split()), flags=re.IGNORECASE)
727
+ def replace_with_null(match):
728
+ func_name = match.group(1)
729
+ alias_part = match.group(3) or ''
730
+ alias_name = match.group(4)
696
731
 
697
- # Perform the replacement with NULL as alias
698
- old_sql = modified_sql
699
- modified_sql, count = pattern_to_sub.subn(f"NULL as {alias_name}", modified_sql)
700
- if count == 0:
701
- print(f"WARNING: NULL replacement failed for NQL call '{original_nql_call}' (no change to SQL). SQL still contains NQL call.")
702
- else:
703
- print(f"DEBUG: Replaced NQL call '{original_nql_call}' with 'NULL as {alias_name}'.")
732
+ # If no alias specified, generate one from function name
733
+ if not alias_name:
734
+ alias_name = f"{func_name}_result"
735
+ alias_part = f" as {alias_name}"
736
+
737
+ print(f"DEBUG: Replacing nql.{func_name}(...) with NULL{alias_part}")
738
+ return f"NULL{alias_part}"
739
+
740
+ modified_sql = re.sub(nql_pattern, replace_with_null, modified_sql, flags=re.IGNORECASE | re.DOTALL)
704
741
 
705
742
  return modified_sql
706
743