npcpy 1.2.35__tar.gz → 1.2.36__tar.gz
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-1.2.35/npcpy.egg-info → npcpy-1.2.36}/PKG-INFO +1 -1
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/npc_compiler.py +50 -17
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/serve.py +523 -77
- {npcpy-1.2.35 → npcpy-1.2.36/npcpy.egg-info}/PKG-INFO +1 -1
- {npcpy-1.2.35 → npcpy-1.2.36}/setup.py +1 -1
- {npcpy-1.2.35 → npcpy-1.2.36}/LICENSE +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/MANIFEST.in +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/README.md +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/__init__.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/data/__init__.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/data/audio.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/data/data_models.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/data/image.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/data/load.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/data/text.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/data/video.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/data/web.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/ft/__init__.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/ft/diff.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/ft/ge.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/ft/memory_trainer.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/ft/model_ensembler.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/ft/rl.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/ft/sft.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/ft/usft.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/gen/__init__.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/gen/audio_gen.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/gen/embeddings.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/gen/image_gen.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/gen/ocr.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/gen/response.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/gen/video_gen.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/llm_funcs.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/main.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/memory/__init__.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/memory/command_history.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/memory/kg_vis.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/memory/knowledge_graph.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/memory/memory_processor.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/memory/search.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/mix/__init__.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/mix/debate.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/npc_sysenv.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/npcs.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/sql/__init__.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/sql/ai_function_tools.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/sql/database_ai_adapters.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/sql/database_ai_functions.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/sql/model_runner.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/sql/npcsql.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/sql/sql_model_compiler.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/tools.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/work/__init__.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/work/desktop.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/work/plan.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy/work/trigger.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy.egg-info/SOURCES.txt +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy.egg-info/dependency_links.txt +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy.egg-info/requires.txt +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/npcpy.egg-info/top_level.txt +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/setup.cfg +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/tests/test_audio.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/tests/test_command_history.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/tests/test_image.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/tests/test_llm_funcs.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/tests/test_load.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/tests/test_npc_compiler.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/tests/test_npcsql.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/tests/test_response.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/tests/test_serve.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/tests/test_text.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/tests/test_tools.py +0 -0
- {npcpy-1.2.35 → npcpy-1.2.36}/tests/test_web.py +0 -0
|
@@ -619,6 +619,35 @@ def load_jinxs_from_directory(directory):
|
|
|
619
619
|
|
|
620
620
|
return jinxs
|
|
621
621
|
|
|
622
|
+
def jinx_to_tool_def(jinx_obj: 'Jinx') -> Dict[str, Any]:
|
|
623
|
+
"""Convert a Jinx instance into an MCP/LLM-compatible tool schema definition."""
|
|
624
|
+
properties: Dict[str, Any] = {}
|
|
625
|
+
required: List[str] = []
|
|
626
|
+
for inp in jinx_obj.inputs:
|
|
627
|
+
if isinstance(inp, str):
|
|
628
|
+
properties[inp] = {"type": "string"}
|
|
629
|
+
required.append(inp)
|
|
630
|
+
elif isinstance(inp, dict):
|
|
631
|
+
name = list(inp.keys())[0]
|
|
632
|
+
properties[name] = {"type": "string", "default": inp.get(name, "")}
|
|
633
|
+
required.append(name)
|
|
634
|
+
return {
|
|
635
|
+
"type": "function",
|
|
636
|
+
"function": {
|
|
637
|
+
"name": jinx_obj.jinx_name,
|
|
638
|
+
"description": jinx_obj.description or f"Jinx: {jinx_obj.jinx_name}",
|
|
639
|
+
"parameters": {
|
|
640
|
+
"type": "object",
|
|
641
|
+
"properties": properties,
|
|
642
|
+
"required": required
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
def build_jinx_tool_catalog(jinxs: Dict[str, 'Jinx']) -> Dict[str, Dict[str, Any]]:
|
|
648
|
+
"""Helper to build a name->tool_def catalog from a dict of Jinx objects."""
|
|
649
|
+
return {name: jinx_to_tool_def(jinx_obj) for name, jinx_obj in jinxs.items()}
|
|
650
|
+
|
|
622
651
|
def get_npc_action_space(npc=None, team=None):
|
|
623
652
|
"""Get action space for NPC including memory CRUD and core capabilities"""
|
|
624
653
|
actions = DEFAULT_ACTION_SPACE.copy()
|
|
@@ -851,6 +880,8 @@ class NPC:
|
|
|
851
880
|
self.tools_schema = []
|
|
852
881
|
self.plain_system_message = plain_system_message
|
|
853
882
|
self.use_global_jinxs = use_global_jinxs
|
|
883
|
+
self.jinx_tool_catalog: Dict[str, Dict[str, Any]] = {}
|
|
884
|
+
self.mcp_servers = []
|
|
854
885
|
|
|
855
886
|
self.memory_length = 20
|
|
856
887
|
self.memory_strategy = 'recent'
|
|
@@ -886,20 +917,20 @@ class NPC:
|
|
|
886
917
|
# If jinxs are explicitly provided *to the NPC* during its standalone creation, load them.
|
|
887
918
|
# This is for NPCs created *outside* a team context initially.
|
|
888
919
|
if jinxs and jinxs != "*":
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
920
|
+
for jinx_item in jinxs:
|
|
921
|
+
if isinstance(jinx_item, Jinx):
|
|
922
|
+
self.jinxs_dict[jinx_item.jinx_name] = jinx_item
|
|
923
|
+
elif isinstance(jinx_item, dict):
|
|
924
|
+
jinx_obj = Jinx(jinx_data=jinx_item)
|
|
925
|
+
self.jinxs_dict[jinx_obj.jinx_name] = jinx_obj
|
|
926
|
+
elif isinstance(jinx_item, str):
|
|
927
|
+
# Try to load from NPC's own directory first
|
|
928
|
+
jinx_path = find_file_path(jinx_item, [self.npc_jinxs_directory], suffix=".jinx")
|
|
929
|
+
if jinx_path:
|
|
930
|
+
jinx_obj = Jinx(jinx_path=jinx_path)
|
|
931
|
+
self.jinxs_dict[jinx_obj.jinx_name] = jinx_obj
|
|
932
|
+
else:
|
|
933
|
+
print(f"Warning: Jinx '{jinx_item}' not found for NPC '{self.name}' during initial load.")
|
|
903
934
|
|
|
904
935
|
self.shared_context = {
|
|
905
936
|
"dataframes": {},
|
|
@@ -977,7 +1008,8 @@ class NPC:
|
|
|
977
1008
|
except Exception as e:
|
|
978
1009
|
print(f"Error performing first-pass rendering for NPC Jinx '{raw_npc_jinx.jinx_name}': {e}")
|
|
979
1010
|
|
|
980
|
-
|
|
1011
|
+
self.jinx_tool_catalog = build_jinx_tool_catalog(self.jinxs_dict)
|
|
1012
|
+
print(f"NPC {self.name} loaded {len(self.jinxs_dict)} jinxs and built catalog with {len(self.jinx_tool_catalog)} tools.")
|
|
981
1013
|
|
|
982
1014
|
def _load_npc_kg(self):
|
|
983
1015
|
"""Load knowledge graph data for this NPC from database"""
|
|
@@ -2004,6 +2036,7 @@ class Team:
|
|
|
2004
2036
|
self.sub_teams: Dict[str, 'Team'] = {}
|
|
2005
2037
|
self.jinxs_dict: Dict[str, 'Jinx'] = {} # This will store first-pass rendered Jinx objects
|
|
2006
2038
|
self._raw_jinxs_list: List['Jinx'] = [] # Temporary storage for raw Team-level Jinx objects
|
|
2039
|
+
self.jinx_tool_catalog: Dict[str, Dict[str, Any]] = {} # Jinx-derived tool defs ready for MCP/LLM
|
|
2007
2040
|
|
|
2008
2041
|
self.jinja_env_for_first_pass = Environment(undefined=SilentUndefined) # Env for macro expansion
|
|
2009
2042
|
|
|
@@ -2059,6 +2092,8 @@ class Team:
|
|
|
2059
2092
|
|
|
2060
2093
|
# Perform first-pass rendering for team-level jinxs
|
|
2061
2094
|
self._perform_first_pass_jinx_rendering()
|
|
2095
|
+
self.jinx_tool_catalog = build_jinx_tool_catalog(self.jinxs_dict)
|
|
2096
|
+
print(f"[TEAM] Built Jinx tool catalog with {len(self.jinx_tool_catalog)} entries for team {self.name}")
|
|
2062
2097
|
|
|
2063
2098
|
# Now, initialize jinxs for all NPCs, as team-level jinxs are ready
|
|
2064
2099
|
for npc_obj in self.npcs.values():
|
|
@@ -2236,8 +2271,6 @@ class Team:
|
|
|
2236
2271
|
self.jinxs_dict[raw_jinx.jinx_name] = raw_jinx # Store the first-pass rendered Jinx
|
|
2237
2272
|
except Exception as e:
|
|
2238
2273
|
print(f"Error performing first-pass rendering for Jinx '{raw_jinx.jinx_name}': {e}")
|
|
2239
|
-
|
|
2240
|
-
self._raw_jinxs_list = [] # Clear temporary storage
|
|
2241
2274
|
|
|
2242
2275
|
|
|
2243
2276
|
def update_context(self, messages: list):
|
|
@@ -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
|
|
@@ -62,14 +67,12 @@ from npcpy.memory.command_history import (
|
|
|
62
67
|
save_conversation_message,
|
|
63
68
|
generate_message_id,
|
|
64
69
|
)
|
|
65
|
-
from npcpy.npc_compiler import Jinx, NPC, Team
|
|
70
|
+
from npcpy.npc_compiler import Jinx, NPC, Team, load_jinxs_from_directory, build_jinx_tool_catalog
|
|
66
71
|
|
|
67
72
|
from npcpy.llm_funcs import (
|
|
68
73
|
get_llm_response, check_llm_command
|
|
69
74
|
)
|
|
70
|
-
from
|
|
71
|
-
import base64
|
|
72
|
-
|
|
75
|
+
from termcolor import cprint
|
|
73
76
|
from npcpy.tools import auto_tools
|
|
74
77
|
|
|
75
78
|
import json
|
|
@@ -86,6 +89,159 @@ cancellation_flags = {}
|
|
|
86
89
|
cancellation_lock = threading.Lock()
|
|
87
90
|
|
|
88
91
|
|
|
92
|
+
# Minimal MCP client (inlined from npcsh corca to avoid corca import)
|
|
93
|
+
class MCPClientNPC:
|
|
94
|
+
def __init__(self, debug: bool = True):
|
|
95
|
+
self.debug = debug
|
|
96
|
+
self.session: Optional[ClientSession] = None
|
|
97
|
+
try:
|
|
98
|
+
self._loop = asyncio.get_event_loop()
|
|
99
|
+
if self._loop.is_closed():
|
|
100
|
+
self._loop = asyncio.new_event_loop()
|
|
101
|
+
asyncio.set_event_loop(self._loop)
|
|
102
|
+
except RuntimeError:
|
|
103
|
+
self._loop = asyncio.new_event_loop()
|
|
104
|
+
asyncio.set_event_loop(self._loop)
|
|
105
|
+
self._exit_stack = self._loop.run_until_complete(AsyncExitStack().__aenter__())
|
|
106
|
+
self.available_tools_llm: List[Dict[str, Any]] = []
|
|
107
|
+
self.tool_map: Dict[str, Callable] = {}
|
|
108
|
+
self.server_script_path: Optional[str] = None
|
|
109
|
+
|
|
110
|
+
def _log(self, message: str, color: str = "cyan") -> None:
|
|
111
|
+
if self.debug:
|
|
112
|
+
cprint(f"[MCP Client] {message}", color, file=sys.stderr)
|
|
113
|
+
|
|
114
|
+
async def _connect_async(self, server_script_path: str) -> None:
|
|
115
|
+
self._log(f"Attempting to connect to MCP server: {server_script_path}")
|
|
116
|
+
self.server_script_path = server_script_path
|
|
117
|
+
abs_path = os.path.abspath(server_script_path)
|
|
118
|
+
if not os.path.exists(abs_path):
|
|
119
|
+
raise FileNotFoundError(f"MCP server script not found: {abs_path}")
|
|
120
|
+
|
|
121
|
+
if abs_path.endswith('.py'):
|
|
122
|
+
cmd_parts = [sys.executable, abs_path]
|
|
123
|
+
elif os.access(abs_path, os.X_OK):
|
|
124
|
+
cmd_parts = [abs_path]
|
|
125
|
+
else:
|
|
126
|
+
raise ValueError(f"Unsupported MCP server script type or not executable: {abs_path}")
|
|
127
|
+
|
|
128
|
+
server_params = StdioServerParameters(
|
|
129
|
+
command=cmd_parts[0],
|
|
130
|
+
args=[abs_path],
|
|
131
|
+
env=os.environ.copy(),
|
|
132
|
+
cwd=os.path.dirname(abs_path) or "."
|
|
133
|
+
)
|
|
134
|
+
if self.session:
|
|
135
|
+
await self._exit_stack.aclose()
|
|
136
|
+
|
|
137
|
+
self._exit_stack = AsyncExitStack()
|
|
138
|
+
|
|
139
|
+
stdio_transport = await self._exit_stack.enter_async_context(stdio_client(server_params))
|
|
140
|
+
self.session = await self._exit_stack.enter_async_context(ClientSession(*stdio_transport))
|
|
141
|
+
await self.session.initialize()
|
|
142
|
+
|
|
143
|
+
response = await self.session.list_tools()
|
|
144
|
+
self.available_tools_llm = []
|
|
145
|
+
self.tool_map = {}
|
|
146
|
+
|
|
147
|
+
if response.tools:
|
|
148
|
+
for mcp_tool in response.tools:
|
|
149
|
+
tool_def = {
|
|
150
|
+
"type": "function",
|
|
151
|
+
"function": {
|
|
152
|
+
"name": mcp_tool.name,
|
|
153
|
+
"description": mcp_tool.description or f"MCP tool: {mcp_tool.name}",
|
|
154
|
+
"parameters": getattr(mcp_tool, "inputSchema", {"type": "object", "properties": {}})
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
self.available_tools_llm.append(tool_def)
|
|
158
|
+
|
|
159
|
+
def make_tool_func(tool_name_closure):
|
|
160
|
+
async def tool_func(**kwargs):
|
|
161
|
+
if not self.session:
|
|
162
|
+
return {"error": "No MCP session"}
|
|
163
|
+
self._log(f"About to call MCP tool {tool_name_closure}")
|
|
164
|
+
try:
|
|
165
|
+
cleaned_kwargs = {k: (None if v == 'None' else v) for k, v in kwargs.items()}
|
|
166
|
+
result = await asyncio.wait_for(
|
|
167
|
+
self.session.call_tool(tool_name_closure, cleaned_kwargs),
|
|
168
|
+
timeout=30.0
|
|
169
|
+
)
|
|
170
|
+
self._log(f"MCP tool {tool_name_closure} returned: {type(result)}")
|
|
171
|
+
return result
|
|
172
|
+
except asyncio.TimeoutError:
|
|
173
|
+
self._log(f"Tool {tool_name_closure} timed out after 30 seconds", "red")
|
|
174
|
+
return {"error": f"Tool {tool_name_closure} timed out"}
|
|
175
|
+
except Exception as e:
|
|
176
|
+
self._log(f"Tool {tool_name_closure} error: {e}", "red")
|
|
177
|
+
return {"error": str(e)}
|
|
178
|
+
|
|
179
|
+
def sync_wrapper(**kwargs):
|
|
180
|
+
self._log(f"Sync wrapper called for {tool_name_closure}")
|
|
181
|
+
return self._loop.run_until_complete(tool_func(**kwargs))
|
|
182
|
+
|
|
183
|
+
return sync_wrapper
|
|
184
|
+
|
|
185
|
+
self.tool_map[mcp_tool.name] = make_tool_func(mcp_tool.name)
|
|
186
|
+
tool_names = list(self.tool_map.keys())
|
|
187
|
+
self._log(f"Connection successful. Tools: {', '.join(tool_names) if tool_names else 'None'}")
|
|
188
|
+
|
|
189
|
+
def connect_sync(self, server_script_path: str) -> bool:
|
|
190
|
+
loop = self._loop
|
|
191
|
+
if loop.is_closed():
|
|
192
|
+
self._loop = asyncio.new_event_loop()
|
|
193
|
+
asyncio.set_event_loop(self._loop)
|
|
194
|
+
loop = self._loop
|
|
195
|
+
try:
|
|
196
|
+
loop.run_until_complete(self._connect_async(server_script_path))
|
|
197
|
+
return True
|
|
198
|
+
except Exception as e:
|
|
199
|
+
cprint(f"MCP connection failed: {e}", "red", file=sys.stderr)
|
|
200
|
+
return False
|
|
201
|
+
|
|
202
|
+
def disconnect_sync(self):
|
|
203
|
+
if self.session:
|
|
204
|
+
self._log("Disconnecting MCP session.")
|
|
205
|
+
loop = self._loop
|
|
206
|
+
if not loop.is_closed():
|
|
207
|
+
try:
|
|
208
|
+
async def close_session():
|
|
209
|
+
await self.session.close()
|
|
210
|
+
await self._exit_stack.aclose()
|
|
211
|
+
loop.run_until_complete(close_session())
|
|
212
|
+
except RuntimeError:
|
|
213
|
+
pass
|
|
214
|
+
except Exception as e:
|
|
215
|
+
print(f"Error during MCP client disconnect: {e}", file=sys.stderr)
|
|
216
|
+
self.session = None
|
|
217
|
+
self._exit_stack = None
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def get_llm_response_with_handling(prompt, npc, messages, tools, stream, team, context=None):
|
|
221
|
+
"""Unified LLM response with basic exception handling (inlined from corca to avoid that dependency)."""
|
|
222
|
+
try:
|
|
223
|
+
return get_llm_response(
|
|
224
|
+
prompt=prompt,
|
|
225
|
+
npc=npc,
|
|
226
|
+
messages=messages,
|
|
227
|
+
tools=tools,
|
|
228
|
+
auto_process_tool_calls=False,
|
|
229
|
+
stream=stream,
|
|
230
|
+
team=team,
|
|
231
|
+
context=context
|
|
232
|
+
)
|
|
233
|
+
except Exception:
|
|
234
|
+
# Fallback retry without context compression logic to keep it simple here.
|
|
235
|
+
return get_llm_response(
|
|
236
|
+
prompt=prompt,
|
|
237
|
+
npc=npc,
|
|
238
|
+
messages=messages,
|
|
239
|
+
tools=tools,
|
|
240
|
+
auto_process_tool_calls=False,
|
|
241
|
+
stream=stream,
|
|
242
|
+
team=team,
|
|
243
|
+
context=context
|
|
244
|
+
)
|
|
89
245
|
class MCPServerManager:
|
|
90
246
|
"""
|
|
91
247
|
Simple in-process tracker for launching/stopping MCP servers.
|
|
@@ -2488,11 +2644,13 @@ def generate_images():
|
|
|
2488
2644
|
if os.path.exists(image_path):
|
|
2489
2645
|
try:
|
|
2490
2646
|
pil_img = Image.open(image_path)
|
|
2647
|
+
pil_img = pil_img.convert("RGB")
|
|
2648
|
+
pil_img.thumbnail((1024, 1024))
|
|
2491
2649
|
input_images.append(pil_img)
|
|
2492
2650
|
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2651
|
+
compressed_bytes = BytesIO()
|
|
2652
|
+
pil_img.save(compressed_bytes, format="JPEG", quality=85, optimize=True)
|
|
2653
|
+
img_data = compressed_bytes.getvalue()
|
|
2496
2654
|
attachments_loaded.append({
|
|
2497
2655
|
"name": os.path.basename(image_path),
|
|
2498
2656
|
"type": "images",
|
|
@@ -2620,6 +2778,7 @@ def get_mcp_tools():
|
|
|
2620
2778
|
return jsonify({"error": "MCP Client (npcsh.corca) not available. Ensure npcsh.corca is installed and importable."}), 500
|
|
2621
2779
|
|
|
2622
2780
|
temp_mcp_client = None
|
|
2781
|
+
jinx_tools = []
|
|
2623
2782
|
try:
|
|
2624
2783
|
|
|
2625
2784
|
if conversation_id and npc_name and hasattr(app, 'corca_states'):
|
|
@@ -2640,6 +2799,25 @@ def get_mcp_tools():
|
|
|
2640
2799
|
temp_mcp_client = MCPClientNPC()
|
|
2641
2800
|
if temp_mcp_client.connect_sync(server_path):
|
|
2642
2801
|
tools = temp_mcp_client.available_tools_llm
|
|
2802
|
+
# Append Jinx-derived tools discovered from global/project jinxs
|
|
2803
|
+
try:
|
|
2804
|
+
jinx_dirs = []
|
|
2805
|
+
if current_path_arg:
|
|
2806
|
+
proj_jinx_dir = os.path.join(os.path.abspath(current_path_arg), "npc_team", "jinxs")
|
|
2807
|
+
if os.path.isdir(proj_jinx_dir):
|
|
2808
|
+
jinx_dirs.append(proj_jinx_dir)
|
|
2809
|
+
global_jinx_dir = os.path.expanduser("~/.npcsh/npc_team/jinxs")
|
|
2810
|
+
if os.path.isdir(global_jinx_dir):
|
|
2811
|
+
jinx_dirs.append(global_jinx_dir)
|
|
2812
|
+
all_jinxs = []
|
|
2813
|
+
for d in jinx_dirs:
|
|
2814
|
+
all_jinxs.extend(load_jinxs_from_directory(d))
|
|
2815
|
+
if all_jinxs:
|
|
2816
|
+
jinx_tools = list(build_jinx_tool_catalog({j.jinx_name: j for j in all_jinxs}).values())
|
|
2817
|
+
print(f"[MCP] Discovered {len(jinx_tools)} Jinx tools for listing.")
|
|
2818
|
+
tools = tools + jinx_tools
|
|
2819
|
+
except Exception as e:
|
|
2820
|
+
print(f"[MCP] Error discovering Jinx tools for listing: {e}")
|
|
2643
2821
|
if selected_names:
|
|
2644
2822
|
tools = [t for t in tools if t.get("function", {}).get("name") in selected_names]
|
|
2645
2823
|
return jsonify({"tools": tools, "error": None})
|
|
@@ -2944,6 +3122,8 @@ def stream():
|
|
|
2944
3122
|
|
|
2945
3123
|
commandstr = data.get("commandstr")
|
|
2946
3124
|
conversation_id = data.get("conversationId")
|
|
3125
|
+
if not conversation_id:
|
|
3126
|
+
return jsonify({"error": "conversationId is required"}), 400
|
|
2947
3127
|
model = data.get("model", None)
|
|
2948
3128
|
provider = data.get("provider", None)
|
|
2949
3129
|
if provider is None:
|
|
@@ -2961,6 +3141,7 @@ def stream():
|
|
|
2961
3141
|
npc_object = None
|
|
2962
3142
|
team_object = None
|
|
2963
3143
|
team = None
|
|
3144
|
+
tool_results_for_db = []
|
|
2964
3145
|
if npc_name:
|
|
2965
3146
|
if hasattr(app, 'registered_teams'):
|
|
2966
3147
|
for team_name, team_object in app.registered_teams.items():
|
|
@@ -3199,83 +3380,257 @@ def stream():
|
|
|
3199
3380
|
)
|
|
3200
3381
|
messages = state.messages
|
|
3201
3382
|
|
|
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
|
-
|
|
3383
|
+
elif exe_mode == 'tool_agent':
|
|
3384
|
+
mcp_server_path_from_request = data.get("mcpServerPath")
|
|
3385
|
+
selected_mcp_tools_from_request = data.get("selectedMcpTools", [])
|
|
3386
|
+
|
|
3387
|
+
# Resolve MCP server path (explicit -> team ctx -> default resolver)
|
|
3388
|
+
effective_mcp_server_path = mcp_server_path_from_request
|
|
3389
|
+
if not effective_mcp_server_path and team_object and hasattr(team_object, 'team_ctx') and team_object.team_ctx:
|
|
3390
|
+
mcp_servers_list = team_object.team_ctx.get('mcp_servers', [])
|
|
3391
|
+
if mcp_servers_list and isinstance(mcp_servers_list, list):
|
|
3392
|
+
first_server_obj = next((s for s in mcp_servers_list if isinstance(s, dict) and 'value' in s), None)
|
|
3393
|
+
if first_server_obj:
|
|
3394
|
+
effective_mcp_server_path = first_server_obj['value']
|
|
3395
|
+
elif isinstance(team_object.team_ctx.get('mcp_server'), str):
|
|
3396
|
+
effective_mcp_server_path = team_object.team_ctx.get('mcp_server')
|
|
3397
|
+
|
|
3398
|
+
effective_mcp_server_path = resolve_mcp_server_path(
|
|
3399
|
+
current_path=current_path,
|
|
3400
|
+
explicit_path=effective_mcp_server_path,
|
|
3401
|
+
force_global=False
|
|
3402
|
+
)
|
|
3403
|
+
print(f"[MCP] effective server path: {effective_mcp_server_path}")
|
|
3404
|
+
|
|
3405
|
+
if not hasattr(app, 'mcp_clients'):
|
|
3406
|
+
app.mcp_clients = {}
|
|
3407
|
+
|
|
3408
|
+
state_key = f"{conversation_id}_{npc_name or 'default'}"
|
|
3409
|
+
client_entry = app.mcp_clients.get(state_key)
|
|
3410
|
+
|
|
3411
|
+
if not client_entry or not client_entry.get("client") or not client_entry["client"].session \
|
|
3412
|
+
or client_entry.get("server_path") != effective_mcp_server_path:
|
|
3413
|
+
mcp_client = MCPClientNPC()
|
|
3414
|
+
if effective_mcp_server_path and mcp_client.connect_sync(effective_mcp_server_path):
|
|
3415
|
+
print(f"[MCP] connected client for {state_key} to {effective_mcp_server_path}")
|
|
3416
|
+
app.mcp_clients[state_key] = {
|
|
3417
|
+
"client": mcp_client,
|
|
3418
|
+
"server_path": effective_mcp_server_path,
|
|
3419
|
+
"messages": messages
|
|
3420
|
+
}
|
|
3421
|
+
else:
|
|
3422
|
+
print(f"[MCP] Failed to connect client for {state_key} to {effective_mcp_server_path}")
|
|
3423
|
+
app.mcp_clients[state_key] = {
|
|
3424
|
+
"client": None,
|
|
3425
|
+
"server_path": effective_mcp_server_path,
|
|
3426
|
+
"messages": messages
|
|
3427
|
+
}
|
|
3233
3428
|
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3429
|
+
mcp_client = app.mcp_clients[state_key]["client"]
|
|
3430
|
+
messages = app.mcp_clients[state_key].get("messages", messages)
|
|
3431
|
+
|
|
3432
|
+
def stream_mcp_sse():
|
|
3433
|
+
nonlocal messages
|
|
3434
|
+
iteration = 0
|
|
3435
|
+
prompt = commandstr
|
|
3436
|
+
while iteration < 10:
|
|
3437
|
+
iteration += 1
|
|
3438
|
+
print(f"[MCP] iteration {iteration} prompt len={len(prompt)}")
|
|
3439
|
+
jinx_tool_catalog = {}
|
|
3440
|
+
if npc_object and hasattr(npc_object, "jinx_tool_catalog"):
|
|
3441
|
+
jinx_tool_catalog = npc_object.jinx_tool_catalog or {}
|
|
3442
|
+
tools_for_llm = []
|
|
3443
|
+
if mcp_client:
|
|
3444
|
+
tools_for_llm.extend(mcp_client.available_tools_llm)
|
|
3445
|
+
# append Jinx-derived tools
|
|
3446
|
+
tools_for_llm.extend(list(jinx_tool_catalog.values()))
|
|
3447
|
+
if selected_mcp_tools_from_request:
|
|
3448
|
+
tools_for_llm = [t for t in tools_for_llm if t["function"]["name"] in selected_mcp_tools_from_request]
|
|
3449
|
+
print(f"[MCP] tools_for_llm: {[t['function']['name'] for t in tools_for_llm]}")
|
|
3450
|
+
|
|
3451
|
+
llm_response = get_llm_response_with_handling(
|
|
3452
|
+
prompt=prompt,
|
|
3238
3453
|
npc=npc_object,
|
|
3454
|
+
messages=messages,
|
|
3455
|
+
tools=tools_for_llm,
|
|
3456
|
+
stream=True,
|
|
3239
3457
|
team=team_object,
|
|
3240
|
-
|
|
3241
|
-
mcp_server_path=effective_mcp_server_path
|
|
3458
|
+
context=f' The users working directory is {current_path}'
|
|
3242
3459
|
)
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3460
|
+
|
|
3461
|
+
stream = llm_response.get("response", [])
|
|
3462
|
+
messages = llm_response.get("messages", messages)
|
|
3463
|
+
collected_content = ""
|
|
3464
|
+
collected_tool_calls = []
|
|
3465
|
+
|
|
3466
|
+
for response_chunk in stream:
|
|
3467
|
+
with cancellation_lock:
|
|
3468
|
+
if cancellation_flags.get(stream_id, False):
|
|
3469
|
+
yield {"type": "interrupt"}
|
|
3470
|
+
return
|
|
3471
|
+
|
|
3472
|
+
if hasattr(response_chunk, "choices") and response_chunk.choices:
|
|
3473
|
+
delta = response_chunk.choices[0].delta
|
|
3474
|
+
if hasattr(delta, "content") and delta.content:
|
|
3475
|
+
collected_content += delta.content
|
|
3476
|
+
chunk_data = {
|
|
3477
|
+
"id": getattr(response_chunk, "id", None),
|
|
3478
|
+
"object": getattr(response_chunk, "object", None),
|
|
3479
|
+
"created": getattr(response_chunk, "created", datetime.datetime.now().strftime('YYYY-DD-MM-HHMMSS')),
|
|
3480
|
+
"model": getattr(response_chunk, "model", model),
|
|
3481
|
+
"choices": [
|
|
3482
|
+
{
|
|
3483
|
+
"index": 0,
|
|
3484
|
+
"delta": {
|
|
3485
|
+
"content": delta.content,
|
|
3486
|
+
"role": "assistant"
|
|
3487
|
+
},
|
|
3488
|
+
"finish_reason": None
|
|
3489
|
+
}
|
|
3490
|
+
]
|
|
3491
|
+
}
|
|
3492
|
+
yield chunk_data
|
|
3493
|
+
|
|
3494
|
+
if hasattr(delta, "tool_calls") and delta.tool_calls:
|
|
3495
|
+
for tool_call_delta in delta.tool_calls:
|
|
3496
|
+
idx = getattr(tool_call_delta, "index", 0)
|
|
3497
|
+
while len(collected_tool_calls) <= idx:
|
|
3498
|
+
collected_tool_calls.append({
|
|
3499
|
+
"id": "",
|
|
3500
|
+
"type": "function",
|
|
3501
|
+
"function": {"name": "", "arguments": ""}
|
|
3502
|
+
})
|
|
3503
|
+
if getattr(tool_call_delta, "id", None):
|
|
3504
|
+
collected_tool_calls[idx]["id"] = tool_call_delta.id
|
|
3505
|
+
if hasattr(tool_call_delta, "function"):
|
|
3506
|
+
fn = tool_call_delta.function
|
|
3507
|
+
if getattr(fn, "name", None):
|
|
3508
|
+
collected_tool_calls[idx]["function"]["name"] = fn.name
|
|
3509
|
+
if getattr(fn, "arguments", None):
|
|
3510
|
+
collected_tool_calls[idx]["function"]["arguments"] += fn.arguments
|
|
3511
|
+
|
|
3512
|
+
if not collected_tool_calls:
|
|
3513
|
+
print("[MCP] no tool calls, finishing streaming loop")
|
|
3514
|
+
break
|
|
3515
|
+
|
|
3516
|
+
print(f"[MCP] collected tool calls: {[tc['function']['name'] for tc in collected_tool_calls]}")
|
|
3517
|
+
yield {
|
|
3518
|
+
"type": "tool_execution_start",
|
|
3519
|
+
"tool_calls": [
|
|
3520
|
+
{
|
|
3521
|
+
"name": tc["function"]["name"],
|
|
3522
|
+
"id": tc["id"],
|
|
3523
|
+
"function": {
|
|
3524
|
+
"name": tc["function"]["name"],
|
|
3525
|
+
"arguments": tc["function"].get("arguments", "")
|
|
3526
|
+
}
|
|
3527
|
+
} for tc in collected_tool_calls
|
|
3528
|
+
]
|
|
3529
|
+
}
|
|
3530
|
+
|
|
3531
|
+
tool_results = []
|
|
3532
|
+
for tc in collected_tool_calls:
|
|
3533
|
+
tool_name = tc["function"]["name"]
|
|
3534
|
+
tool_args = tc["function"]["arguments"]
|
|
3535
|
+
tool_id = tc["id"]
|
|
3536
|
+
|
|
3537
|
+
if isinstance(tool_args, str):
|
|
3538
|
+
try:
|
|
3539
|
+
tool_args = json.loads(tool_args) if tool_args.strip() else {}
|
|
3540
|
+
except json.JSONDecodeError:
|
|
3541
|
+
tool_args = {}
|
|
3542
|
+
|
|
3543
|
+
print(f"[MCP] tool_start {tool_name} args={tool_args}")
|
|
3544
|
+
yield {"type": "tool_start", "name": tool_name, "id": tool_id, "args": tool_args}
|
|
3545
|
+
try:
|
|
3546
|
+
tool_content = ""
|
|
3547
|
+
# First, try local Jinx execution
|
|
3548
|
+
if npc_object and hasattr(npc_object, "jinxs_dict") and tool_name in npc_object.jinxs_dict:
|
|
3549
|
+
jinx_obj = npc_object.jinxs_dict[tool_name]
|
|
3550
|
+
try:
|
|
3551
|
+
jinx_ctx = jinx_obj.execute(
|
|
3552
|
+
input_values=tool_args if isinstance(tool_args, dict) else {},
|
|
3553
|
+
npc=npc_object,
|
|
3554
|
+
messages=messages
|
|
3555
|
+
)
|
|
3556
|
+
tool_content = str(jinx_ctx.get("output", jinx_ctx))
|
|
3557
|
+
print(f"[MCP] jinx tool_complete {tool_name}")
|
|
3558
|
+
except Exception as e:
|
|
3559
|
+
raise Exception(f"Jinx execution failed: {e}")
|
|
3266
3560
|
else:
|
|
3267
|
-
|
|
3268
|
-
|
|
3561
|
+
try:
|
|
3562
|
+
loop = asyncio.get_event_loop()
|
|
3563
|
+
except RuntimeError:
|
|
3564
|
+
loop = asyncio.new_event_loop()
|
|
3565
|
+
asyncio.set_event_loop(loop)
|
|
3566
|
+
if loop.is_closed():
|
|
3567
|
+
loop = asyncio.new_event_loop()
|
|
3568
|
+
asyncio.set_event_loop(loop)
|
|
3569
|
+
mcp_result = loop.run_until_complete(
|
|
3570
|
+
mcp_client.session.call_tool(tool_name, tool_args)
|
|
3571
|
+
) if mcp_client else {"error": "No MCP client"}
|
|
3572
|
+
if hasattr(mcp_result, "content") and mcp_result.content:
|
|
3573
|
+
for content_item in mcp_result.content:
|
|
3574
|
+
if hasattr(content_item, "text"):
|
|
3575
|
+
tool_content += content_item.text
|
|
3576
|
+
elif hasattr(content_item, "data"):
|
|
3577
|
+
tool_content += str(content_item.data)
|
|
3578
|
+
else:
|
|
3579
|
+
tool_content += str(content_item)
|
|
3580
|
+
else:
|
|
3581
|
+
tool_content = str(mcp_result)
|
|
3582
|
+
|
|
3583
|
+
tool_results.append({
|
|
3584
|
+
"role": "tool",
|
|
3585
|
+
"tool_call_id": tool_id,
|
|
3586
|
+
"name": tool_name,
|
|
3587
|
+
"content": tool_content
|
|
3588
|
+
})
|
|
3589
|
+
|
|
3590
|
+
print(f"[MCP] tool_complete {tool_name}")
|
|
3591
|
+
yield {"type": "tool_complete", "name": tool_name, "id": tool_id, "result_preview": tool_content[:4000]}
|
|
3592
|
+
except Exception as e:
|
|
3593
|
+
err_msg = f"Error executing {tool_name}: {e}"
|
|
3594
|
+
tool_results.append({
|
|
3595
|
+
"role": "tool",
|
|
3596
|
+
"tool_call_id": tool_id,
|
|
3597
|
+
"name": tool_name,
|
|
3598
|
+
"content": err_msg
|
|
3599
|
+
})
|
|
3600
|
+
print(f"[MCP] tool_error {tool_name}: {e}")
|
|
3601
|
+
yield {"type": "tool_error", "name": tool_name, "id": tool_id, "error": str(e)}
|
|
3602
|
+
|
|
3603
|
+
serialized_tool_calls = []
|
|
3604
|
+
for tc in collected_tool_calls:
|
|
3605
|
+
parsed_args = tc["function"]["arguments"]
|
|
3606
|
+
# Gemini/LLM expects arguments as JSON string, not dict
|
|
3607
|
+
if isinstance(parsed_args, dict):
|
|
3608
|
+
args_for_message = json.dumps(parsed_args)
|
|
3609
|
+
else:
|
|
3610
|
+
args_for_message = str(parsed_args)
|
|
3611
|
+
serialized_tool_calls.append({
|
|
3612
|
+
"id": tc["id"],
|
|
3613
|
+
"type": tc["type"],
|
|
3614
|
+
"function": {
|
|
3615
|
+
"name": tc["function"]["name"],
|
|
3616
|
+
"arguments": args_for_message
|
|
3617
|
+
}
|
|
3618
|
+
})
|
|
3269
3619
|
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3620
|
+
messages.append({
|
|
3621
|
+
"role": "assistant",
|
|
3622
|
+
"content": collected_content,
|
|
3623
|
+
"tool_calls": serialized_tool_calls
|
|
3624
|
+
})
|
|
3625
|
+
messages.extend(tool_results)
|
|
3626
|
+
tool_results_for_db = tool_results
|
|
3627
|
+
|
|
3628
|
+
prompt = ""
|
|
3276
3629
|
|
|
3277
|
-
app.
|
|
3278
|
-
|
|
3630
|
+
app.mcp_clients[state_key]["messages"] = messages
|
|
3631
|
+
return
|
|
3632
|
+
|
|
3633
|
+
stream_response = stream_mcp_sse()
|
|
3279
3634
|
|
|
3280
3635
|
else:
|
|
3281
3636
|
stream_response = {"output": f"Unsupported execution mode: {exe_mode}", "messages": messages}
|
|
@@ -3316,6 +3671,36 @@ def stream():
|
|
|
3316
3671
|
tool_call_data = {"id": None, "function_name": None, "arguments": ""}
|
|
3317
3672
|
|
|
3318
3673
|
try:
|
|
3674
|
+
# New: handle generators (tool_agent streaming)
|
|
3675
|
+
if hasattr(stream_response, "__iter__") and not isinstance(stream_response, (dict, str)):
|
|
3676
|
+
for chunk in stream_response:
|
|
3677
|
+
with cancellation_lock:
|
|
3678
|
+
if cancellation_flags.get(current_stream_id, False):
|
|
3679
|
+
interrupted = True
|
|
3680
|
+
break
|
|
3681
|
+
if chunk is None:
|
|
3682
|
+
continue
|
|
3683
|
+
if isinstance(chunk, dict):
|
|
3684
|
+
if chunk.get("type") == "interrupt":
|
|
3685
|
+
interrupted = True
|
|
3686
|
+
break
|
|
3687
|
+
yield f"data: {json.dumps(chunk)}\n\n"
|
|
3688
|
+
if chunk.get("choices"):
|
|
3689
|
+
for choice in chunk["choices"]:
|
|
3690
|
+
delta = choice.get("delta", {})
|
|
3691
|
+
content_piece = delta.get("content")
|
|
3692
|
+
if content_piece:
|
|
3693
|
+
complete_response.append(content_piece)
|
|
3694
|
+
continue
|
|
3695
|
+
yield f"data: {json.dumps({'choices':[{'delta':{'content': str(chunk), 'role': 'assistant'},'finish_reason':None}]})}\n\n"
|
|
3696
|
+
# ensure stream termination and cleanup for generator flows
|
|
3697
|
+
yield "data: [DONE]\n\n"
|
|
3698
|
+
with cancellation_lock:
|
|
3699
|
+
if current_stream_id in cancellation_flags:
|
|
3700
|
+
del cancellation_flags[current_stream_id]
|
|
3701
|
+
print(f"Cleaned up cancellation flag for stream ID: {current_stream_id}")
|
|
3702
|
+
return
|
|
3703
|
+
|
|
3319
3704
|
if isinstance(stream_response, str) :
|
|
3320
3705
|
print('stream a str and not a gen')
|
|
3321
3706
|
chunk_data = {
|
|
@@ -3429,6 +3814,36 @@ def stream():
|
|
|
3429
3814
|
# Yield message_stop immediately so the client's stream ends quickly
|
|
3430
3815
|
yield f"data: {json.dumps({'type': 'message_stop'})}\n\n"
|
|
3431
3816
|
|
|
3817
|
+
# Persist tool call metadata and results before final assistant content
|
|
3818
|
+
if tool_call_data.get("function_name") or tool_call_data.get("arguments"):
|
|
3819
|
+
save_conversation_message(
|
|
3820
|
+
command_history,
|
|
3821
|
+
conversation_id,
|
|
3822
|
+
"assistant",
|
|
3823
|
+
{"tool_call": tool_call_data},
|
|
3824
|
+
wd=current_path,
|
|
3825
|
+
model=model,
|
|
3826
|
+
provider=provider,
|
|
3827
|
+
npc=npc_name,
|
|
3828
|
+
team=team,
|
|
3829
|
+
message_id=generate_message_id(),
|
|
3830
|
+
)
|
|
3831
|
+
|
|
3832
|
+
if tool_results_for_db:
|
|
3833
|
+
for tr in tool_results_for_db:
|
|
3834
|
+
save_conversation_message(
|
|
3835
|
+
command_history,
|
|
3836
|
+
conversation_id,
|
|
3837
|
+
"tool",
|
|
3838
|
+
{"tool_name": tr.get("name"), "tool_call_id": tr.get("tool_call_id"), "content": tr.get("content")},
|
|
3839
|
+
wd=current_path,
|
|
3840
|
+
model=model,
|
|
3841
|
+
provider=provider,
|
|
3842
|
+
npc=npc_name,
|
|
3843
|
+
team=team,
|
|
3844
|
+
message_id=generate_message_id(),
|
|
3845
|
+
)
|
|
3846
|
+
|
|
3432
3847
|
# Save assistant message to the database
|
|
3433
3848
|
npc_name_to_save = npc_object.name if npc_object else ''
|
|
3434
3849
|
save_conversation_message(
|
|
@@ -3682,6 +4097,37 @@ def ollama_status():
|
|
|
3682
4097
|
return jsonify({"status": "not_found"})
|
|
3683
4098
|
|
|
3684
4099
|
|
|
4100
|
+
@app.route("/api/ollama/tool_models", methods=["GET"])
|
|
4101
|
+
def get_ollama_tool_models():
|
|
4102
|
+
"""
|
|
4103
|
+
Best-effort detection of Ollama models whose templates include tool-call support.
|
|
4104
|
+
We scan templates for tool placeholders; if none are found we assume tools are unsupported.
|
|
4105
|
+
"""
|
|
4106
|
+
try:
|
|
4107
|
+
detected = []
|
|
4108
|
+
listing = ollama.list()
|
|
4109
|
+
for model in listing.get("models", []):
|
|
4110
|
+
name = getattr(model, "model", None) or model.get("name") if isinstance(model, dict) else None
|
|
4111
|
+
if not name:
|
|
4112
|
+
continue
|
|
4113
|
+
try:
|
|
4114
|
+
details = ollama.show(name)
|
|
4115
|
+
tmpl = details.get("template") or ""
|
|
4116
|
+
if "{{- if .Tools" in tmpl or "{{- range .Tools" in tmpl or "{{- if .ToolCalls" in tmpl:
|
|
4117
|
+
detected.append(name)
|
|
4118
|
+
continue
|
|
4119
|
+
metadata = details.get("metadata") or {}
|
|
4120
|
+
if metadata.get("tools") or metadata.get("tool_calls"):
|
|
4121
|
+
detected.append(name)
|
|
4122
|
+
except Exception as inner_e:
|
|
4123
|
+
print(f"Warning: could not inspect ollama model {name} for tool support: {inner_e}")
|
|
4124
|
+
continue
|
|
4125
|
+
return jsonify({"models": detected, "error": None})
|
|
4126
|
+
except Exception as e:
|
|
4127
|
+
print(f"Error listing Ollama tool-capable models: {e}")
|
|
4128
|
+
return jsonify({"models": [], "error": str(e)}), 500
|
|
4129
|
+
|
|
4130
|
+
|
|
3685
4131
|
@app.route('/api/ollama/models', methods=['GET'])
|
|
3686
4132
|
def get_ollama_models():
|
|
3687
4133
|
response = ollama.list()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|