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/__init__.py +10 -2
- npcpy/gen/image_gen.py +5 -2
- npcpy/gen/response.py +262 -64
- npcpy/llm_funcs.py +478 -832
- npcpy/ml_funcs.py +746 -0
- npcpy/npc_array.py +1294 -0
- npcpy/npc_compiler.py +522 -341
- npcpy/npc_sysenv.py +17 -2
- npcpy/serve.py +162 -14
- npcpy/sql/npcsql.py +96 -59
- {npcpy-1.2.36.dist-info → npcpy-1.3.1.dist-info}/METADATA +175 -4
- {npcpy-1.2.36.dist-info → npcpy-1.3.1.dist-info}/RECORD +15 -13
- {npcpy-1.2.36.dist-info → npcpy-1.3.1.dist-info}/WHEEL +0 -0
- {npcpy-1.2.36.dist-info → npcpy-1.3.1.dist-info}/licenses/LICENSE +0 -0
- {npcpy-1.2.36.dist-info → npcpy-1.3.1.dist-info}/top_level.txt +0 -0
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
|
-
|
|
846
|
-
|
|
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
|
-
'
|
|
976
|
-
'
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
#
|
|
366
|
-
|
|
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
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
673
|
-
|
|
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
|
|
680
|
-
This is used for the fallback path
|
|
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
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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
|
-
#
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
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
|
|