npcpy 1.2.35__py3-none-any.whl → 1.2.37__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 +348 -252
- npcpy/npc_sysenv.py +17 -2
- npcpy/serve.py +684 -90
- npcpy/sql/npcsql.py +96 -59
- {npcpy-1.2.35.dist-info → npcpy-1.2.37.dist-info}/METADATA +173 -1
- {npcpy-1.2.35.dist-info → npcpy-1.2.37.dist-info}/RECORD +15 -13
- {npcpy-1.2.35.dist-info → npcpy-1.2.37.dist-info}/WHEEL +0 -0
- {npcpy-1.2.35.dist-info → npcpy-1.2.37.dist-info}/licenses/LICENSE +0 -0
- {npcpy-1.2.35.dist-info → npcpy-1.2.37.dist-info}/top_level.txt +0 -0
npcpy/serve.py
CHANGED
|
@@ -9,6 +9,9 @@ import traceback
|
|
|
9
9
|
import glob
|
|
10
10
|
import re
|
|
11
11
|
import time
|
|
12
|
+
import asyncio
|
|
13
|
+
from typing import Optional, List, Dict, Callable, Any
|
|
14
|
+
from contextlib import AsyncExitStack
|
|
12
15
|
|
|
13
16
|
import io
|
|
14
17
|
from flask_cors import CORS
|
|
@@ -18,6 +21,8 @@ import json
|
|
|
18
21
|
from pathlib import Path
|
|
19
22
|
import yaml
|
|
20
23
|
from dotenv import load_dotenv
|
|
24
|
+
from mcp import ClientSession, StdioServerParameters
|
|
25
|
+
from mcp.client.stdio import stdio_client
|
|
21
26
|
|
|
22
27
|
from PIL import Image
|
|
23
28
|
from PIL import ImageFile
|
|
@@ -45,7 +50,6 @@ from npcpy.memory.search import execute_rag_command, execute_brainblast_command
|
|
|
45
50
|
from npcpy.data.load import load_file_contents
|
|
46
51
|
from npcpy.data.web import search_web
|
|
47
52
|
|
|
48
|
-
from npcsh._state import get_relevant_memories, search_kg_facts
|
|
49
53
|
|
|
50
54
|
import base64
|
|
51
55
|
import shutil
|
|
@@ -62,14 +66,12 @@ from npcpy.memory.command_history import (
|
|
|
62
66
|
save_conversation_message,
|
|
63
67
|
generate_message_id,
|
|
64
68
|
)
|
|
65
|
-
from npcpy.npc_compiler import Jinx, NPC, Team
|
|
69
|
+
from npcpy.npc_compiler import Jinx, NPC, Team, load_jinxs_from_directory, build_jinx_tool_catalog, initialize_npc_project
|
|
66
70
|
|
|
67
71
|
from npcpy.llm_funcs import (
|
|
68
72
|
get_llm_response, check_llm_command
|
|
69
73
|
)
|
|
70
|
-
from
|
|
71
|
-
import base64
|
|
72
|
-
|
|
74
|
+
from termcolor import cprint
|
|
73
75
|
from npcpy.tools import auto_tools
|
|
74
76
|
|
|
75
77
|
import json
|
|
@@ -86,6 +88,159 @@ cancellation_flags = {}
|
|
|
86
88
|
cancellation_lock = threading.Lock()
|
|
87
89
|
|
|
88
90
|
|
|
91
|
+
# Minimal MCP client (inlined from npcsh corca to avoid corca import)
|
|
92
|
+
class MCPClientNPC:
|
|
93
|
+
def __init__(self, debug: bool = True):
|
|
94
|
+
self.debug = debug
|
|
95
|
+
self.session: Optional[ClientSession] = None
|
|
96
|
+
try:
|
|
97
|
+
self._loop = asyncio.get_event_loop()
|
|
98
|
+
if self._loop.is_closed():
|
|
99
|
+
self._loop = asyncio.new_event_loop()
|
|
100
|
+
asyncio.set_event_loop(self._loop)
|
|
101
|
+
except RuntimeError:
|
|
102
|
+
self._loop = asyncio.new_event_loop()
|
|
103
|
+
asyncio.set_event_loop(self._loop)
|
|
104
|
+
self._exit_stack = self._loop.run_until_complete(AsyncExitStack().__aenter__())
|
|
105
|
+
self.available_tools_llm: List[Dict[str, Any]] = []
|
|
106
|
+
self.tool_map: Dict[str, Callable] = {}
|
|
107
|
+
self.server_script_path: Optional[str] = None
|
|
108
|
+
|
|
109
|
+
def _log(self, message: str, color: str = "cyan") -> None:
|
|
110
|
+
if self.debug:
|
|
111
|
+
cprint(f"[MCP Client] {message}", color, file=sys.stderr)
|
|
112
|
+
|
|
113
|
+
async def _connect_async(self, server_script_path: str) -> None:
|
|
114
|
+
self._log(f"Attempting to connect to MCP server: {server_script_path}")
|
|
115
|
+
self.server_script_path = server_script_path
|
|
116
|
+
abs_path = os.path.abspath(server_script_path)
|
|
117
|
+
if not os.path.exists(abs_path):
|
|
118
|
+
raise FileNotFoundError(f"MCP server script not found: {abs_path}")
|
|
119
|
+
|
|
120
|
+
if abs_path.endswith('.py'):
|
|
121
|
+
cmd_parts = [sys.executable, abs_path]
|
|
122
|
+
elif os.access(abs_path, os.X_OK):
|
|
123
|
+
cmd_parts = [abs_path]
|
|
124
|
+
else:
|
|
125
|
+
raise ValueError(f"Unsupported MCP server script type or not executable: {abs_path}")
|
|
126
|
+
|
|
127
|
+
server_params = StdioServerParameters(
|
|
128
|
+
command=cmd_parts[0],
|
|
129
|
+
args=[abs_path],
|
|
130
|
+
env=os.environ.copy(),
|
|
131
|
+
cwd=os.path.dirname(abs_path) or "."
|
|
132
|
+
)
|
|
133
|
+
if self.session:
|
|
134
|
+
await self._exit_stack.aclose()
|
|
135
|
+
|
|
136
|
+
self._exit_stack = AsyncExitStack()
|
|
137
|
+
|
|
138
|
+
stdio_transport = await self._exit_stack.enter_async_context(stdio_client(server_params))
|
|
139
|
+
self.session = await self._exit_stack.enter_async_context(ClientSession(*stdio_transport))
|
|
140
|
+
await self.session.initialize()
|
|
141
|
+
|
|
142
|
+
response = await self.session.list_tools()
|
|
143
|
+
self.available_tools_llm = []
|
|
144
|
+
self.tool_map = {}
|
|
145
|
+
|
|
146
|
+
if response.tools:
|
|
147
|
+
for mcp_tool in response.tools:
|
|
148
|
+
tool_def = {
|
|
149
|
+
"type": "function",
|
|
150
|
+
"function": {
|
|
151
|
+
"name": mcp_tool.name,
|
|
152
|
+
"description": mcp_tool.description or f"MCP tool: {mcp_tool.name}",
|
|
153
|
+
"parameters": getattr(mcp_tool, "inputSchema", {"type": "object", "properties": {}})
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
self.available_tools_llm.append(tool_def)
|
|
157
|
+
|
|
158
|
+
def make_tool_func(tool_name_closure):
|
|
159
|
+
async def tool_func(**kwargs):
|
|
160
|
+
if not self.session:
|
|
161
|
+
return {"error": "No MCP session"}
|
|
162
|
+
self._log(f"About to call MCP tool {tool_name_closure}")
|
|
163
|
+
try:
|
|
164
|
+
cleaned_kwargs = {k: (None if v == 'None' else v) for k, v in kwargs.items()}
|
|
165
|
+
result = await asyncio.wait_for(
|
|
166
|
+
self.session.call_tool(tool_name_closure, cleaned_kwargs),
|
|
167
|
+
timeout=30.0
|
|
168
|
+
)
|
|
169
|
+
self._log(f"MCP tool {tool_name_closure} returned: {type(result)}")
|
|
170
|
+
return result
|
|
171
|
+
except asyncio.TimeoutError:
|
|
172
|
+
self._log(f"Tool {tool_name_closure} timed out after 30 seconds", "red")
|
|
173
|
+
return {"error": f"Tool {tool_name_closure} timed out"}
|
|
174
|
+
except Exception as e:
|
|
175
|
+
self._log(f"Tool {tool_name_closure} error: {e}", "red")
|
|
176
|
+
return {"error": str(e)}
|
|
177
|
+
|
|
178
|
+
def sync_wrapper(**kwargs):
|
|
179
|
+
self._log(f"Sync wrapper called for {tool_name_closure}")
|
|
180
|
+
return self._loop.run_until_complete(tool_func(**kwargs))
|
|
181
|
+
|
|
182
|
+
return sync_wrapper
|
|
183
|
+
|
|
184
|
+
self.tool_map[mcp_tool.name] = make_tool_func(mcp_tool.name)
|
|
185
|
+
tool_names = list(self.tool_map.keys())
|
|
186
|
+
self._log(f"Connection successful. Tools: {', '.join(tool_names) if tool_names else 'None'}")
|
|
187
|
+
|
|
188
|
+
def connect_sync(self, server_script_path: str) -> bool:
|
|
189
|
+
loop = self._loop
|
|
190
|
+
if loop.is_closed():
|
|
191
|
+
self._loop = asyncio.new_event_loop()
|
|
192
|
+
asyncio.set_event_loop(self._loop)
|
|
193
|
+
loop = self._loop
|
|
194
|
+
try:
|
|
195
|
+
loop.run_until_complete(self._connect_async(server_script_path))
|
|
196
|
+
return True
|
|
197
|
+
except Exception as e:
|
|
198
|
+
cprint(f"MCP connection failed: {e}", "red", file=sys.stderr)
|
|
199
|
+
return False
|
|
200
|
+
|
|
201
|
+
def disconnect_sync(self):
|
|
202
|
+
if self.session:
|
|
203
|
+
self._log("Disconnecting MCP session.")
|
|
204
|
+
loop = self._loop
|
|
205
|
+
if not loop.is_closed():
|
|
206
|
+
try:
|
|
207
|
+
async def close_session():
|
|
208
|
+
await self.session.close()
|
|
209
|
+
await self._exit_stack.aclose()
|
|
210
|
+
loop.run_until_complete(close_session())
|
|
211
|
+
except RuntimeError:
|
|
212
|
+
pass
|
|
213
|
+
except Exception as e:
|
|
214
|
+
print(f"Error during MCP client disconnect: {e}", file=sys.stderr)
|
|
215
|
+
self.session = None
|
|
216
|
+
self._exit_stack = None
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def get_llm_response_with_handling(prompt, npc, messages, tools, stream, team, context=None):
|
|
220
|
+
"""Unified LLM response with basic exception handling (inlined from corca to avoid that dependency)."""
|
|
221
|
+
try:
|
|
222
|
+
return get_llm_response(
|
|
223
|
+
prompt=prompt,
|
|
224
|
+
npc=npc,
|
|
225
|
+
messages=messages,
|
|
226
|
+
tools=tools,
|
|
227
|
+
auto_process_tool_calls=False,
|
|
228
|
+
stream=stream,
|
|
229
|
+
team=team,
|
|
230
|
+
context=context
|
|
231
|
+
)
|
|
232
|
+
except Exception:
|
|
233
|
+
# Fallback retry without context compression logic to keep it simple here.
|
|
234
|
+
return get_llm_response(
|
|
235
|
+
prompt=prompt,
|
|
236
|
+
npc=npc,
|
|
237
|
+
messages=messages,
|
|
238
|
+
tools=tools,
|
|
239
|
+
auto_process_tool_calls=False,
|
|
240
|
+
stream=stream,
|
|
241
|
+
team=team,
|
|
242
|
+
context=context
|
|
243
|
+
)
|
|
89
244
|
class MCPServerManager:
|
|
90
245
|
"""
|
|
91
246
|
Simple in-process tracker for launching/stopping MCP servers.
|
|
@@ -816,12 +971,8 @@ def execute_jinx():
|
|
|
816
971
|
'state': state,
|
|
817
972
|
'CommandHistory': CommandHistory,
|
|
818
973
|
'load_kg_from_db': load_kg_from_db,
|
|
819
|
-
'
|
|
820
|
-
'
|
|
821
|
-
'load_file_contents': load_file_contents,
|
|
822
|
-
'search_web': search_web,
|
|
823
|
-
'get_relevant_memories': get_relevant_memories,
|
|
824
|
-
'search_kg_facts': search_kg_facts,
|
|
974
|
+
#'get_relevant_memories': get_relevant_memories,
|
|
975
|
+
#'search_kg_facts': search_kg_facts,
|
|
825
976
|
}
|
|
826
977
|
|
|
827
978
|
jinx_execution_result = jinx.execute(
|
|
@@ -1765,6 +1916,134 @@ def get_jinxs_project():
|
|
|
1765
1916
|
print(jinx_data)
|
|
1766
1917
|
return jsonify({"jinxs": jinx_data, "error": None})
|
|
1767
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
|
+
|
|
1768
2047
|
@app.route("/api/npc_team_global")
|
|
1769
2048
|
def get_npc_team_global():
|
|
1770
2049
|
global_npc_directory = os.path.expanduser("~/.npcsh/npc_team")
|
|
@@ -1894,19 +2173,27 @@ def api_get_last_used_in_conversation():
|
|
|
1894
2173
|
result = get_last_used_model_and_npc_in_conversation(conversation_id)
|
|
1895
2174
|
return jsonify(result)
|
|
1896
2175
|
|
|
1897
|
-
def get_ctx_path(is_global, current_path=None):
|
|
2176
|
+
def get_ctx_path(is_global, current_path=None, create_default=False):
|
|
1898
2177
|
"""Determines the path to the .ctx file."""
|
|
1899
2178
|
if is_global:
|
|
1900
2179
|
ctx_dir = os.path.join(os.path.expanduser("~/.npcsh/npc_team/"))
|
|
1901
2180
|
ctx_files = glob.glob(os.path.join(ctx_dir, "*.ctx"))
|
|
1902
|
-
|
|
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
|
|
1903
2186
|
else:
|
|
1904
2187
|
if not current_path:
|
|
1905
2188
|
return None
|
|
1906
|
-
|
|
2189
|
+
|
|
1907
2190
|
ctx_dir = os.path.join(current_path, "npc_team")
|
|
1908
2191
|
ctx_files = glob.glob(os.path.join(ctx_dir, "*.ctx"))
|
|
1909
|
-
|
|
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
|
|
1910
2197
|
|
|
1911
2198
|
|
|
1912
2199
|
def read_ctx_file(file_path):
|
|
@@ -2007,10 +2294,10 @@ def save_project_context():
|
|
|
2007
2294
|
data = request.json
|
|
2008
2295
|
current_path = data.get("path")
|
|
2009
2296
|
context_data = data.get("context", {})
|
|
2010
|
-
|
|
2297
|
+
|
|
2011
2298
|
if not current_path:
|
|
2012
2299
|
return jsonify({"error": "Project path is required."}), 400
|
|
2013
|
-
|
|
2300
|
+
|
|
2014
2301
|
ctx_path = get_ctx_path(is_global=False, current_path=current_path)
|
|
2015
2302
|
if write_ctx_file(ctx_path, context_data):
|
|
2016
2303
|
return jsonify({"message": "Project context saved.", "error": None})
|
|
@@ -2020,6 +2307,23 @@ def save_project_context():
|
|
|
2020
2307
|
print(f"Error saving project context: {e}")
|
|
2021
2308
|
return jsonify({"error": str(e)}), 500
|
|
2022
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
|
+
|
|
2023
2327
|
|
|
2024
2328
|
|
|
2025
2329
|
|
|
@@ -2488,11 +2792,13 @@ def generate_images():
|
|
|
2488
2792
|
if os.path.exists(image_path):
|
|
2489
2793
|
try:
|
|
2490
2794
|
pil_img = Image.open(image_path)
|
|
2795
|
+
pil_img = pil_img.convert("RGB")
|
|
2796
|
+
pil_img.thumbnail((1024, 1024))
|
|
2491
2797
|
input_images.append(pil_img)
|
|
2492
2798
|
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2799
|
+
compressed_bytes = BytesIO()
|
|
2800
|
+
pil_img.save(compressed_bytes, format="JPEG", quality=85, optimize=True)
|
|
2801
|
+
img_data = compressed_bytes.getvalue()
|
|
2496
2802
|
attachments_loaded.append({
|
|
2497
2803
|
"name": os.path.basename(image_path),
|
|
2498
2804
|
"type": "images",
|
|
@@ -2620,6 +2926,7 @@ def get_mcp_tools():
|
|
|
2620
2926
|
return jsonify({"error": "MCP Client (npcsh.corca) not available. Ensure npcsh.corca is installed and importable."}), 500
|
|
2621
2927
|
|
|
2622
2928
|
temp_mcp_client = None
|
|
2929
|
+
jinx_tools = []
|
|
2623
2930
|
try:
|
|
2624
2931
|
|
|
2625
2932
|
if conversation_id and npc_name and hasattr(app, 'corca_states'):
|
|
@@ -2640,6 +2947,25 @@ def get_mcp_tools():
|
|
|
2640
2947
|
temp_mcp_client = MCPClientNPC()
|
|
2641
2948
|
if temp_mcp_client.connect_sync(server_path):
|
|
2642
2949
|
tools = temp_mcp_client.available_tools_llm
|
|
2950
|
+
# Append Jinx-derived tools discovered from global/project jinxs
|
|
2951
|
+
try:
|
|
2952
|
+
jinx_dirs = []
|
|
2953
|
+
if current_path_arg:
|
|
2954
|
+
proj_jinx_dir = os.path.join(os.path.abspath(current_path_arg), "npc_team", "jinxs")
|
|
2955
|
+
if os.path.isdir(proj_jinx_dir):
|
|
2956
|
+
jinx_dirs.append(proj_jinx_dir)
|
|
2957
|
+
global_jinx_dir = os.path.expanduser("~/.npcsh/npc_team/jinxs")
|
|
2958
|
+
if os.path.isdir(global_jinx_dir):
|
|
2959
|
+
jinx_dirs.append(global_jinx_dir)
|
|
2960
|
+
all_jinxs = []
|
|
2961
|
+
for d in jinx_dirs:
|
|
2962
|
+
all_jinxs.extend(load_jinxs_from_directory(d))
|
|
2963
|
+
if all_jinxs:
|
|
2964
|
+
jinx_tools = list(build_jinx_tool_catalog({j.jinx_name: j for j in all_jinxs}).values())
|
|
2965
|
+
print(f"[MCP] Discovered {len(jinx_tools)} Jinx tools for listing.")
|
|
2966
|
+
tools = tools + jinx_tools
|
|
2967
|
+
except Exception as e:
|
|
2968
|
+
print(f"[MCP] Error discovering Jinx tools for listing: {e}")
|
|
2643
2969
|
if selected_names:
|
|
2644
2970
|
tools = [t for t in tools if t.get("function", {}).get("name") in selected_names]
|
|
2645
2971
|
return jsonify({"tools": tools, "error": None})
|
|
@@ -2944,6 +3270,8 @@ def stream():
|
|
|
2944
3270
|
|
|
2945
3271
|
commandstr = data.get("commandstr")
|
|
2946
3272
|
conversation_id = data.get("conversationId")
|
|
3273
|
+
if not conversation_id:
|
|
3274
|
+
return jsonify({"error": "conversationId is required"}), 400
|
|
2947
3275
|
model = data.get("model", None)
|
|
2948
3276
|
provider = data.get("provider", None)
|
|
2949
3277
|
if provider is None:
|
|
@@ -2961,6 +3289,7 @@ def stream():
|
|
|
2961
3289
|
npc_object = None
|
|
2962
3290
|
team_object = None
|
|
2963
3291
|
team = None
|
|
3292
|
+
tool_results_for_db = []
|
|
2964
3293
|
if npc_name:
|
|
2965
3294
|
if hasattr(app, 'registered_teams'):
|
|
2966
3295
|
for team_name, team_object in app.registered_teams.items():
|
|
@@ -3199,83 +3528,257 @@ def stream():
|
|
|
3199
3528
|
)
|
|
3200
3529
|
messages = state.messages
|
|
3201
3530
|
|
|
3202
|
-
elif exe_mode == '
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3531
|
+
elif exe_mode == 'tool_agent':
|
|
3532
|
+
mcp_server_path_from_request = data.get("mcpServerPath")
|
|
3533
|
+
selected_mcp_tools_from_request = data.get("selectedMcpTools", [])
|
|
3534
|
+
|
|
3535
|
+
# Resolve MCP server path (explicit -> team ctx -> default resolver)
|
|
3536
|
+
effective_mcp_server_path = mcp_server_path_from_request
|
|
3537
|
+
if not effective_mcp_server_path and team_object and hasattr(team_object, 'team_ctx') and team_object.team_ctx:
|
|
3538
|
+
mcp_servers_list = team_object.team_ctx.get('mcp_servers', [])
|
|
3539
|
+
if mcp_servers_list and isinstance(mcp_servers_list, list):
|
|
3540
|
+
first_server_obj = next((s for s in mcp_servers_list if isinstance(s, dict) and 'value' in s), None)
|
|
3541
|
+
if first_server_obj:
|
|
3542
|
+
effective_mcp_server_path = first_server_obj['value']
|
|
3543
|
+
elif isinstance(team_object.team_ctx.get('mcp_server'), str):
|
|
3544
|
+
effective_mcp_server_path = team_object.team_ctx.get('mcp_server')
|
|
3545
|
+
|
|
3546
|
+
effective_mcp_server_path = resolve_mcp_server_path(
|
|
3547
|
+
current_path=current_path,
|
|
3548
|
+
explicit_path=effective_mcp_server_path,
|
|
3549
|
+
force_global=False
|
|
3550
|
+
)
|
|
3551
|
+
print(f"[MCP] effective server path: {effective_mcp_server_path}")
|
|
3552
|
+
|
|
3553
|
+
if not hasattr(app, 'mcp_clients'):
|
|
3554
|
+
app.mcp_clients = {}
|
|
3555
|
+
|
|
3556
|
+
state_key = f"{conversation_id}_{npc_name or 'default'}"
|
|
3557
|
+
client_entry = app.mcp_clients.get(state_key)
|
|
3558
|
+
|
|
3559
|
+
if not client_entry or not client_entry.get("client") or not client_entry["client"].session \
|
|
3560
|
+
or client_entry.get("server_path") != effective_mcp_server_path:
|
|
3561
|
+
mcp_client = MCPClientNPC()
|
|
3562
|
+
if effective_mcp_server_path and mcp_client.connect_sync(effective_mcp_server_path):
|
|
3563
|
+
print(f"[MCP] connected client for {state_key} to {effective_mcp_server_path}")
|
|
3564
|
+
app.mcp_clients[state_key] = {
|
|
3565
|
+
"client": mcp_client,
|
|
3566
|
+
"server_path": effective_mcp_server_path,
|
|
3567
|
+
"messages": messages
|
|
3568
|
+
}
|
|
3569
|
+
else:
|
|
3570
|
+
print(f"[MCP] Failed to connect client for {state_key} to {effective_mcp_server_path}")
|
|
3571
|
+
app.mcp_clients[state_key] = {
|
|
3572
|
+
"client": None,
|
|
3573
|
+
"server_path": effective_mcp_server_path,
|
|
3574
|
+
"messages": messages
|
|
3575
|
+
}
|
|
3233
3576
|
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3577
|
+
mcp_client = app.mcp_clients[state_key]["client"]
|
|
3578
|
+
messages = app.mcp_clients[state_key].get("messages", messages)
|
|
3579
|
+
|
|
3580
|
+
def stream_mcp_sse():
|
|
3581
|
+
nonlocal messages
|
|
3582
|
+
iteration = 0
|
|
3583
|
+
prompt = commandstr
|
|
3584
|
+
while iteration < 10:
|
|
3585
|
+
iteration += 1
|
|
3586
|
+
print(f"[MCP] iteration {iteration} prompt len={len(prompt)}")
|
|
3587
|
+
jinx_tool_catalog = {}
|
|
3588
|
+
if npc_object and hasattr(npc_object, "jinx_tool_catalog"):
|
|
3589
|
+
jinx_tool_catalog = npc_object.jinx_tool_catalog or {}
|
|
3590
|
+
tools_for_llm = []
|
|
3591
|
+
if mcp_client:
|
|
3592
|
+
tools_for_llm.extend(mcp_client.available_tools_llm)
|
|
3593
|
+
# append Jinx-derived tools
|
|
3594
|
+
tools_for_llm.extend(list(jinx_tool_catalog.values()))
|
|
3595
|
+
if selected_mcp_tools_from_request:
|
|
3596
|
+
tools_for_llm = [t for t in tools_for_llm if t["function"]["name"] in selected_mcp_tools_from_request]
|
|
3597
|
+
print(f"[MCP] tools_for_llm: {[t['function']['name'] for t in tools_for_llm]}")
|
|
3598
|
+
|
|
3599
|
+
llm_response = get_llm_response_with_handling(
|
|
3600
|
+
prompt=prompt,
|
|
3238
3601
|
npc=npc_object,
|
|
3602
|
+
messages=messages,
|
|
3603
|
+
tools=tools_for_llm,
|
|
3604
|
+
stream=True,
|
|
3239
3605
|
team=team_object,
|
|
3240
|
-
|
|
3241
|
-
mcp_server_path=effective_mcp_server_path
|
|
3606
|
+
context=f' The users working directory is {current_path}'
|
|
3242
3607
|
)
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3608
|
+
|
|
3609
|
+
stream = llm_response.get("response", [])
|
|
3610
|
+
messages = llm_response.get("messages", messages)
|
|
3611
|
+
collected_content = ""
|
|
3612
|
+
collected_tool_calls = []
|
|
3613
|
+
|
|
3614
|
+
for response_chunk in stream:
|
|
3615
|
+
with cancellation_lock:
|
|
3616
|
+
if cancellation_flags.get(stream_id, False):
|
|
3617
|
+
yield {"type": "interrupt"}
|
|
3618
|
+
return
|
|
3619
|
+
|
|
3620
|
+
if hasattr(response_chunk, "choices") and response_chunk.choices:
|
|
3621
|
+
delta = response_chunk.choices[0].delta
|
|
3622
|
+
if hasattr(delta, "content") and delta.content:
|
|
3623
|
+
collected_content += delta.content
|
|
3624
|
+
chunk_data = {
|
|
3625
|
+
"id": getattr(response_chunk, "id", None),
|
|
3626
|
+
"object": getattr(response_chunk, "object", None),
|
|
3627
|
+
"created": getattr(response_chunk, "created", datetime.datetime.now().strftime('YYYY-DD-MM-HHMMSS')),
|
|
3628
|
+
"model": getattr(response_chunk, "model", model),
|
|
3629
|
+
"choices": [
|
|
3630
|
+
{
|
|
3631
|
+
"index": 0,
|
|
3632
|
+
"delta": {
|
|
3633
|
+
"content": delta.content,
|
|
3634
|
+
"role": "assistant"
|
|
3635
|
+
},
|
|
3636
|
+
"finish_reason": None
|
|
3637
|
+
}
|
|
3638
|
+
]
|
|
3639
|
+
}
|
|
3640
|
+
yield chunk_data
|
|
3641
|
+
|
|
3642
|
+
if hasattr(delta, "tool_calls") and delta.tool_calls:
|
|
3643
|
+
for tool_call_delta in delta.tool_calls:
|
|
3644
|
+
idx = getattr(tool_call_delta, "index", 0)
|
|
3645
|
+
while len(collected_tool_calls) <= idx:
|
|
3646
|
+
collected_tool_calls.append({
|
|
3647
|
+
"id": "",
|
|
3648
|
+
"type": "function",
|
|
3649
|
+
"function": {"name": "", "arguments": ""}
|
|
3650
|
+
})
|
|
3651
|
+
if getattr(tool_call_delta, "id", None):
|
|
3652
|
+
collected_tool_calls[idx]["id"] = tool_call_delta.id
|
|
3653
|
+
if hasattr(tool_call_delta, "function"):
|
|
3654
|
+
fn = tool_call_delta.function
|
|
3655
|
+
if getattr(fn, "name", None):
|
|
3656
|
+
collected_tool_calls[idx]["function"]["name"] = fn.name
|
|
3657
|
+
if getattr(fn, "arguments", None):
|
|
3658
|
+
collected_tool_calls[idx]["function"]["arguments"] += fn.arguments
|
|
3659
|
+
|
|
3660
|
+
if not collected_tool_calls:
|
|
3661
|
+
print("[MCP] no tool calls, finishing streaming loop")
|
|
3662
|
+
break
|
|
3663
|
+
|
|
3664
|
+
print(f"[MCP] collected tool calls: {[tc['function']['name'] for tc in collected_tool_calls]}")
|
|
3665
|
+
yield {
|
|
3666
|
+
"type": "tool_execution_start",
|
|
3667
|
+
"tool_calls": [
|
|
3668
|
+
{
|
|
3669
|
+
"name": tc["function"]["name"],
|
|
3670
|
+
"id": tc["id"],
|
|
3671
|
+
"function": {
|
|
3672
|
+
"name": tc["function"]["name"],
|
|
3673
|
+
"arguments": tc["function"].get("arguments", "")
|
|
3674
|
+
}
|
|
3675
|
+
} for tc in collected_tool_calls
|
|
3676
|
+
]
|
|
3677
|
+
}
|
|
3678
|
+
|
|
3679
|
+
tool_results = []
|
|
3680
|
+
for tc in collected_tool_calls:
|
|
3681
|
+
tool_name = tc["function"]["name"]
|
|
3682
|
+
tool_args = tc["function"]["arguments"]
|
|
3683
|
+
tool_id = tc["id"]
|
|
3684
|
+
|
|
3685
|
+
if isinstance(tool_args, str):
|
|
3686
|
+
try:
|
|
3687
|
+
tool_args = json.loads(tool_args) if tool_args.strip() else {}
|
|
3688
|
+
except json.JSONDecodeError:
|
|
3689
|
+
tool_args = {}
|
|
3690
|
+
|
|
3691
|
+
print(f"[MCP] tool_start {tool_name} args={tool_args}")
|
|
3692
|
+
yield {"type": "tool_start", "name": tool_name, "id": tool_id, "args": tool_args}
|
|
3693
|
+
try:
|
|
3694
|
+
tool_content = ""
|
|
3695
|
+
# First, try local Jinx execution
|
|
3696
|
+
if npc_object and hasattr(npc_object, "jinxs_dict") and tool_name in npc_object.jinxs_dict:
|
|
3697
|
+
jinx_obj = npc_object.jinxs_dict[tool_name]
|
|
3698
|
+
try:
|
|
3699
|
+
jinx_ctx = jinx_obj.execute(
|
|
3700
|
+
input_values=tool_args if isinstance(tool_args, dict) else {},
|
|
3701
|
+
npc=npc_object,
|
|
3702
|
+
messages=messages
|
|
3703
|
+
)
|
|
3704
|
+
tool_content = str(jinx_ctx.get("output", jinx_ctx))
|
|
3705
|
+
print(f"[MCP] jinx tool_complete {tool_name}")
|
|
3706
|
+
except Exception as e:
|
|
3707
|
+
raise Exception(f"Jinx execution failed: {e}")
|
|
3266
3708
|
else:
|
|
3267
|
-
|
|
3268
|
-
|
|
3709
|
+
try:
|
|
3710
|
+
loop = asyncio.get_event_loop()
|
|
3711
|
+
except RuntimeError:
|
|
3712
|
+
loop = asyncio.new_event_loop()
|
|
3713
|
+
asyncio.set_event_loop(loop)
|
|
3714
|
+
if loop.is_closed():
|
|
3715
|
+
loop = asyncio.new_event_loop()
|
|
3716
|
+
asyncio.set_event_loop(loop)
|
|
3717
|
+
mcp_result = loop.run_until_complete(
|
|
3718
|
+
mcp_client.session.call_tool(tool_name, tool_args)
|
|
3719
|
+
) if mcp_client else {"error": "No MCP client"}
|
|
3720
|
+
if hasattr(mcp_result, "content") and mcp_result.content:
|
|
3721
|
+
for content_item in mcp_result.content:
|
|
3722
|
+
if hasattr(content_item, "text"):
|
|
3723
|
+
tool_content += content_item.text
|
|
3724
|
+
elif hasattr(content_item, "data"):
|
|
3725
|
+
tool_content += str(content_item.data)
|
|
3726
|
+
else:
|
|
3727
|
+
tool_content += str(content_item)
|
|
3728
|
+
else:
|
|
3729
|
+
tool_content = str(mcp_result)
|
|
3730
|
+
|
|
3731
|
+
tool_results.append({
|
|
3732
|
+
"role": "tool",
|
|
3733
|
+
"tool_call_id": tool_id,
|
|
3734
|
+
"name": tool_name,
|
|
3735
|
+
"content": tool_content
|
|
3736
|
+
})
|
|
3737
|
+
|
|
3738
|
+
print(f"[MCP] tool_complete {tool_name}")
|
|
3739
|
+
yield {"type": "tool_complete", "name": tool_name, "id": tool_id, "result_preview": tool_content[:4000]}
|
|
3740
|
+
except Exception as e:
|
|
3741
|
+
err_msg = f"Error executing {tool_name}: {e}"
|
|
3742
|
+
tool_results.append({
|
|
3743
|
+
"role": "tool",
|
|
3744
|
+
"tool_call_id": tool_id,
|
|
3745
|
+
"name": tool_name,
|
|
3746
|
+
"content": err_msg
|
|
3747
|
+
})
|
|
3748
|
+
print(f"[MCP] tool_error {tool_name}: {e}")
|
|
3749
|
+
yield {"type": "tool_error", "name": tool_name, "id": tool_id, "error": str(e)}
|
|
3750
|
+
|
|
3751
|
+
serialized_tool_calls = []
|
|
3752
|
+
for tc in collected_tool_calls:
|
|
3753
|
+
parsed_args = tc["function"]["arguments"]
|
|
3754
|
+
# Gemini/LLM expects arguments as JSON string, not dict
|
|
3755
|
+
if isinstance(parsed_args, dict):
|
|
3756
|
+
args_for_message = json.dumps(parsed_args)
|
|
3757
|
+
else:
|
|
3758
|
+
args_for_message = str(parsed_args)
|
|
3759
|
+
serialized_tool_calls.append({
|
|
3760
|
+
"id": tc["id"],
|
|
3761
|
+
"type": tc["type"],
|
|
3762
|
+
"function": {
|
|
3763
|
+
"name": tc["function"]["name"],
|
|
3764
|
+
"arguments": args_for_message
|
|
3765
|
+
}
|
|
3766
|
+
})
|
|
3269
3767
|
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3768
|
+
messages.append({
|
|
3769
|
+
"role": "assistant",
|
|
3770
|
+
"content": collected_content,
|
|
3771
|
+
"tool_calls": serialized_tool_calls
|
|
3772
|
+
})
|
|
3773
|
+
messages.extend(tool_results)
|
|
3774
|
+
tool_results_for_db = tool_results
|
|
3775
|
+
|
|
3776
|
+
prompt = ""
|
|
3276
3777
|
|
|
3277
|
-
app.
|
|
3278
|
-
|
|
3778
|
+
app.mcp_clients[state_key]["messages"] = messages
|
|
3779
|
+
return
|
|
3780
|
+
|
|
3781
|
+
stream_response = stream_mcp_sse()
|
|
3279
3782
|
|
|
3280
3783
|
else:
|
|
3281
3784
|
stream_response = {"output": f"Unsupported execution mode: {exe_mode}", "messages": messages}
|
|
@@ -3316,6 +3819,36 @@ def stream():
|
|
|
3316
3819
|
tool_call_data = {"id": None, "function_name": None, "arguments": ""}
|
|
3317
3820
|
|
|
3318
3821
|
try:
|
|
3822
|
+
# New: handle generators (tool_agent streaming)
|
|
3823
|
+
if hasattr(stream_response, "__iter__") and not isinstance(stream_response, (dict, str)):
|
|
3824
|
+
for chunk in stream_response:
|
|
3825
|
+
with cancellation_lock:
|
|
3826
|
+
if cancellation_flags.get(current_stream_id, False):
|
|
3827
|
+
interrupted = True
|
|
3828
|
+
break
|
|
3829
|
+
if chunk is None:
|
|
3830
|
+
continue
|
|
3831
|
+
if isinstance(chunk, dict):
|
|
3832
|
+
if chunk.get("type") == "interrupt":
|
|
3833
|
+
interrupted = True
|
|
3834
|
+
break
|
|
3835
|
+
yield f"data: {json.dumps(chunk)}\n\n"
|
|
3836
|
+
if chunk.get("choices"):
|
|
3837
|
+
for choice in chunk["choices"]:
|
|
3838
|
+
delta = choice.get("delta", {})
|
|
3839
|
+
content_piece = delta.get("content")
|
|
3840
|
+
if content_piece:
|
|
3841
|
+
complete_response.append(content_piece)
|
|
3842
|
+
continue
|
|
3843
|
+
yield f"data: {json.dumps({'choices':[{'delta':{'content': str(chunk), 'role': 'assistant'},'finish_reason':None}]})}\n\n"
|
|
3844
|
+
# ensure stream termination and cleanup for generator flows
|
|
3845
|
+
yield "data: [DONE]\n\n"
|
|
3846
|
+
with cancellation_lock:
|
|
3847
|
+
if current_stream_id in cancellation_flags:
|
|
3848
|
+
del cancellation_flags[current_stream_id]
|
|
3849
|
+
print(f"Cleaned up cancellation flag for stream ID: {current_stream_id}")
|
|
3850
|
+
return
|
|
3851
|
+
|
|
3319
3852
|
if isinstance(stream_response, str) :
|
|
3320
3853
|
print('stream a str and not a gen')
|
|
3321
3854
|
chunk_data = {
|
|
@@ -3429,6 +3962,36 @@ def stream():
|
|
|
3429
3962
|
# Yield message_stop immediately so the client's stream ends quickly
|
|
3430
3963
|
yield f"data: {json.dumps({'type': 'message_stop'})}\n\n"
|
|
3431
3964
|
|
|
3965
|
+
# Persist tool call metadata and results before final assistant content
|
|
3966
|
+
if tool_call_data.get("function_name") or tool_call_data.get("arguments"):
|
|
3967
|
+
save_conversation_message(
|
|
3968
|
+
command_history,
|
|
3969
|
+
conversation_id,
|
|
3970
|
+
"assistant",
|
|
3971
|
+
{"tool_call": tool_call_data},
|
|
3972
|
+
wd=current_path,
|
|
3973
|
+
model=model,
|
|
3974
|
+
provider=provider,
|
|
3975
|
+
npc=npc_name,
|
|
3976
|
+
team=team,
|
|
3977
|
+
message_id=generate_message_id(),
|
|
3978
|
+
)
|
|
3979
|
+
|
|
3980
|
+
if tool_results_for_db:
|
|
3981
|
+
for tr in tool_results_for_db:
|
|
3982
|
+
save_conversation_message(
|
|
3983
|
+
command_history,
|
|
3984
|
+
conversation_id,
|
|
3985
|
+
"tool",
|
|
3986
|
+
{"tool_name": tr.get("name"), "tool_call_id": tr.get("tool_call_id"), "content": tr.get("content")},
|
|
3987
|
+
wd=current_path,
|
|
3988
|
+
model=model,
|
|
3989
|
+
provider=provider,
|
|
3990
|
+
npc=npc_name,
|
|
3991
|
+
team=team,
|
|
3992
|
+
message_id=generate_message_id(),
|
|
3993
|
+
)
|
|
3994
|
+
|
|
3432
3995
|
# Save assistant message to the database
|
|
3433
3996
|
npc_name_to_save = npc_object.name if npc_object else ''
|
|
3434
3997
|
save_conversation_message(
|
|
@@ -3682,6 +4245,37 @@ def ollama_status():
|
|
|
3682
4245
|
return jsonify({"status": "not_found"})
|
|
3683
4246
|
|
|
3684
4247
|
|
|
4248
|
+
@app.route("/api/ollama/tool_models", methods=["GET"])
|
|
4249
|
+
def get_ollama_tool_models():
|
|
4250
|
+
"""
|
|
4251
|
+
Best-effort detection of Ollama models whose templates include tool-call support.
|
|
4252
|
+
We scan templates for tool placeholders; if none are found we assume tools are unsupported.
|
|
4253
|
+
"""
|
|
4254
|
+
try:
|
|
4255
|
+
detected = []
|
|
4256
|
+
listing = ollama.list()
|
|
4257
|
+
for model in listing.get("models", []):
|
|
4258
|
+
name = getattr(model, "model", None) or model.get("name") if isinstance(model, dict) else None
|
|
4259
|
+
if not name:
|
|
4260
|
+
continue
|
|
4261
|
+
try:
|
|
4262
|
+
details = ollama.show(name)
|
|
4263
|
+
tmpl = details.get("template") or ""
|
|
4264
|
+
if "{{- if .Tools" in tmpl or "{{- range .Tools" in tmpl or "{{- if .ToolCalls" in tmpl:
|
|
4265
|
+
detected.append(name)
|
|
4266
|
+
continue
|
|
4267
|
+
metadata = details.get("metadata") or {}
|
|
4268
|
+
if metadata.get("tools") or metadata.get("tool_calls"):
|
|
4269
|
+
detected.append(name)
|
|
4270
|
+
except Exception as inner_e:
|
|
4271
|
+
print(f"Warning: could not inspect ollama model {name} for tool support: {inner_e}")
|
|
4272
|
+
continue
|
|
4273
|
+
return jsonify({"models": detected, "error": None})
|
|
4274
|
+
except Exception as e:
|
|
4275
|
+
print(f"Error listing Ollama tool-capable models: {e}")
|
|
4276
|
+
return jsonify({"models": [], "error": str(e)}), 500
|
|
4277
|
+
|
|
4278
|
+
|
|
3685
4279
|
@app.route('/api/ollama/models', methods=['GET'])
|
|
3686
4280
|
def get_ollama_models():
|
|
3687
4281
|
response = ollama.list()
|