Skip to content

Commit 657ab86

Browse files
committed
pti stream fixes. adding reasoning content to streams.
1 parent 1b90505 commit 657ab86

File tree

8 files changed

+425
-42
lines changed

8 files changed

+425
-42
lines changed

mkdocs.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,6 @@ nav:
4545
- TLDR Cheat Sheet: TLDR_Cheat_sheet.md
4646
- API:
4747
- Image: api/image.md
48-
- LLM Functions: api/llm_funcs.md
48+
- LLM Functions: api/llm_funcs.md
49+
- NPC, Team, Tool: api/npc.md
50+
- NPC Sys Env Helpers: api/npc_sys_env_helpers.md

npcpy/llm_funcs.py

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1025,28 +1025,3 @@ def check_llm_command(
10251025
else:
10261026
print("Error: Invalid action in LLM response")
10271027
return "Error: Invalid action in LLM response"
1028-
1029-
1030-
def rehash_last_message(
1031-
conversation_id: str,
1032-
model: str,
1033-
provider: str,
1034-
npc: Any = None,
1035-
stream: bool = False,
1036-
) -> dict:
1037-
from npcpy.memory.command_history import CommandHistory
1038-
command_history = CommandHistory()
1039-
last_message = command_history.get_last_conversation(conversation_id)
1040-
if last_message is None:
1041-
convo_id = command_history.get_most_recent_conversation_id()[0]
1042-
last_message = command_history.get_last_conversation(convo_id)
1043-
1044-
user_command = last_message[3] # Assuming content is in the 4th column
1045-
return check_llm_command(
1046-
user_command,
1047-
model=model,
1048-
provider=provider,
1049-
npc=npc,
1050-
messages=None,
1051-
stream=stream,
1052-
)

npcpy/memory/command_history.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -882,7 +882,8 @@ def get_available_tables(db_path: str) -> str:
882882
Returns:
883883
str: The available tables in the database.
884884
"""
885-
885+
if '~' in db_path:
886+
db_path = os.path.expanduser(db_path)
886887
try:
887888
with sqlite3.connect(db_path) as conn:
888889
cursor = conn.cursor()

npcpy/modes/pti.py

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ def enter_reasoning_human_in_the_loop(
157157
reasoning_model=reasoning_model,
158158
reasoning_provider=reasoning_provider, answer_only=False)
159159
else:
160-
message= "Think first though and use <think> tags. Once finished, either answer plainly or write a request for input by beginning with the <request_for_input> tag. and close it with a </request_for_input>"
160+
message= "Think first though and use <think> tags in your chain of thought. Once finished, either answer plainly or write a request for input by beginning with the <request_for_input> tag. and close it with a </request_for_input>"
161161
if user_input is None:
162162
user_input = input('user>')
163163

@@ -183,13 +183,31 @@ def enter_reasoning_human_in_the_loop(
183183
assistant_reply, messages = response['response'], response['messages']
184184
thoughts = []
185185
response_chunks = []
186-
in_think_block = False
187-
for chunk in assistant_reply:
186+
in_think_block = False # the thinking chain generated after reasoning
187+
188+
thinking = False # the reasoning content
189+
190+
191+
for chunk in assistant_reply:
192+
if thinking:
193+
if not in_think_block:
194+
in_think_block = True
188195
try:
196+
189197
if reasoning_provider == "ollama":
190198
chunk_content = chunk.get("message", {}).get("content", "")
191199
else:
192-
chunk_content = "".join(
200+
chunk_content = ''
201+
reasoning_content = ''
202+
for c in chunk.choices:
203+
if hasattr(c.delta, "reasoning_content"):
204+
205+
reasoning_content += c.delta.reasoning_content
206+
207+
if reasoning_content:
208+
thinking = True
209+
chunk_content = reasoning_content
210+
chunk_content += "".join(
193211
choice.delta.content
194212
for choice in chunk.choices
195213
if choice.delta.content is not None
@@ -198,14 +216,9 @@ def enter_reasoning_human_in_the_loop(
198216
print(chunk_content, end='')
199217
combined_text = "".join(response_chunks)
200218

201-
# Check for LLM request block
202-
if (
203-
"<think>" in combined_text
204-
and "</think>" not in combined_text
205-
):
206-
in_think_block = True
207-
208219
if in_think_block:
220+
if '</thinking>' in combined_text:
221+
in_think_block = False
209222
thoughts.append(chunk_content)
210223

211224
if "</request_for_input>" in combined_text:

npcpy/npc_sysenv.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1335,11 +1335,15 @@ def print_and_process_stream_with_markdown(response,
13351335
dot_count += 1
13361336

13371337
if provider == "ollama":
1338+
13381339
chunk_content = chunk["message"]["content"]
13391340
else:
1340-
chunk_content = "".join(
1341+
1342+
chunk_content = "".join(c.delta.reasoning_content for c in chunk.choices if c.delta.reasoning_content)
1343+
1344+
chunk_content += "".join(
13411345
c.delta.content for c in chunk.choices if c.delta.content
1342-
)
1346+
)
13431347
if not chunk_content:
13441348
continue
13451349
str_output += chunk_content

npcpy/routes.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,10 @@
1717
)
1818

1919
from npcpy.llm_funcs import (
20+
check_llm_command,
2021
get_llm_response,
21-
execute_llm_command,
2222
rehash_last_message,
2323
gen_image,
24-
handle_tool_call,
2524
generate_video,
2625
)
2726
from npcpy.npc_compiler import NPC, Team, Tool
@@ -382,6 +381,31 @@ def rag_handler(command: str, **kwargs):
382381
traceback.print_exc()
383382
return {"output": f"Error executing RAG command: {e}", "messages": messages}
384383

384+
385+
def rehash_last_message(
386+
conversation_id: str,
387+
model: str,
388+
provider: str,
389+
npc: Any = None,
390+
stream: bool = False,
391+
) -> dict:
392+
from npcpy.memory.command_history import CommandHistory
393+
command_history = CommandHistory()
394+
last_message = command_history.get_last_conversation(conversation_id)
395+
if last_message is None:
396+
convo_id = command_history.get_most_recent_conversation_id()[0]
397+
last_message = command_history.get_last_conversation(convo_id)
398+
399+
user_command = last_message[3] # Assuming content is in the 4th column
400+
return check_llm_command(
401+
user_command,
402+
model=model,
403+
provider=provider,
404+
npc=npc,
405+
messages=None,
406+
stream=stream,
407+
)
408+
385409
@router.route("rehash", "Re-execute the last LLM command with the same input", shell_only=True)
386410
def rehash_handler(command: str, **kwargs):
387411
messages = safe_get(kwargs, "messages", [])

tests/mcp_tool_test.py

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import os
2+
import asyncio
3+
import json
4+
import sys
5+
from contextlib import AsyncExitStack
6+
from typing import Any, Dict, List
7+
8+
# --- MCP Imports ---
9+
try:
10+
from mcp import ClientSession, StdioServerParameters
11+
from mcp.client.stdio import stdio_client
12+
MCP_AVAILABLE = True
13+
except ImportError:
14+
print("FATAL: 'mcp' library is required. Install it (`pip install mcp`).")
15+
exit(1)
16+
17+
# --- Synchronous MCP Tool Execution Helper (Uses asyncio.run) ---
18+
19+
async def _async_call_mcp_tool(
20+
abs_server_path: str,
21+
tool_name: str,
22+
tool_args: Dict,
23+
debug: bool = True # Add debug flag back
24+
) -> Any:
25+
if not MCP_AVAILABLE:
26+
raise ImportError("MCP library not installed.")
27+
28+
result_content: Any = {"error": "MCP call failed to complete"} # Default error
29+
server_name = os.path.basename(abs_server_path) # For logging
30+
31+
# Define log helper inside
32+
def _log(msg):
33+
if debug: print(f"[_async_call_mcp: {server_name}/{tool_name}] {msg}")
34+
35+
_log(f"Attempting to connect to {abs_server_path}...")
36+
command = "python" if abs_server_path.endswith('.py') else "node"
37+
server_params = StdioServerParameters(
38+
command=command,
39+
args=[abs_server_path],
40+
env=os.environ.copy()
41+
)
42+
timeout_seconds = 30.0
43+
44+
try:
45+
async with AsyncExitStack() as stack:
46+
_log(f"Awaiting connect_and_call() with timeout {timeout_seconds}s...")
47+
async def connect_and_call():
48+
nonlocal result_content
49+
_log(f"Entering stdio_client context...")
50+
stdio_transport = await stack.enter_async_context(stdio_client(server_params))
51+
_log(f"Entering ClientSession context...")
52+
session = await stack.enter_async_context(ClientSession(*stdio_transport))
53+
_log(f"Awaiting session.initialize()...")
54+
await session.initialize()
55+
_log(f"Session initialized. Awaiting session.call_tool({tool_name}, {tool_args})...")
56+
call_result = await session.call_tool(tool_name, tool_args)
57+
_log(f"session.call_tool completed. Raw result: {call_result}") # Log raw result
58+
59+
content = call_result.content
60+
_log(f"Extracted content type: {type(content)}")
61+
62+
# --- Corrected Content Handling ---
63+
if isinstance(content, list) and content and all(hasattr(item, 'text') for item in content):
64+
result_content = [item.text for item in content]
65+
_log(f"Processed list of TextContent: {result_content}")
66+
elif isinstance(content, list) and len(content) == 1 and hasattr(content[0], 'text'):
67+
result_content = content[0].text
68+
_log(f"Processed single TextContent in list: {result_content!r}")
69+
elif hasattr(content, 'text'):
70+
result_content = content.text
71+
_log(f"Processed direct TextContent: {result_content!r}")
72+
else:
73+
result_content = content
74+
_log(f"Using content directly (not TextContent): {str(result_content)[:200]}...")
75+
# --- End Corrected Content Handling ---
76+
77+
await asyncio.wait_for(connect_and_call(), timeout=timeout_seconds)
78+
_log(f"connect_and_call() finished successfully.")
79+
80+
except asyncio.TimeoutError:
81+
_log(f"Timeout Error!")
82+
result_content = {"error": f"Timeout executing MCP tool '{tool_name}'"}
83+
except Exception as e:
84+
_log(f"Exception: {type(e).__name__} - {e}")
85+
# import traceback # Optional for more detail
86+
# traceback.print_exc() # Optional
87+
result_content = {"error": f"Error executing MCP tool '{tool_name}': {type(e).__name__} - {e}"}
88+
89+
return result_content
90+
91+
# --- execute_mcp_tool_sync (Passes debug flag) ---
92+
def execute_mcp_tool_sync(
93+
server_path: str,
94+
tool_name: str,
95+
tool_args: Dict,
96+
debug: bool = True # Add debug flag
97+
) -> Any:
98+
if not MCP_AVAILABLE:
99+
return {"error": "MCP library not installed."}
100+
101+
abs_server_path = os.path.abspath(server_path)
102+
if not os.path.exists(abs_server_path):
103+
return {"error": f"Server path not found: {abs_server_path}"}
104+
if not (abs_server_path.endswith('.py') or abs_server_path.endswith('.js')):
105+
return {"error": f"Server path must be .py or .js: {abs_server_path}"}
106+
107+
try:
108+
asyncio.get_running_loop()
109+
if debug: print("[execute_mcp_tool_sync] Error: Cannot run sync within active async loop.")
110+
return {"error": "Cannot run MCP tool sync within active async context."}
111+
except RuntimeError:
112+
pass # No loop running
113+
114+
if debug: print(f"[execute_mcp_tool_sync] Calling asyncio.run for {tool_name} on {os.path.basename(abs_server_path)}...")
115+
try:
116+
# Pass debug flag down
117+
result = asyncio.run(_async_call_mcp_tool(abs_server_path, tool_name, tool_args, debug))
118+
if debug: print(f"[execute_mcp_tool_sync] asyncio.run completed.")
119+
return result
120+
except Exception as e:
121+
if debug: print(f"[execute_mcp_tool_sync] Error during asyncio.run: {e}")
122+
return {"error": f"Failed to run MCP tool '{tool_name}' synchronously: {e}"}
123+
124+
125+
# --- Example Usage (Passes debug=True) ---
126+
if __name__ == "__main__":
127+
if len(sys.argv) != 4:
128+
print(f"Usage: python {sys.argv[0]} <mcp_server_script_path> <tool_name> '<json_arguments>'")
129+
print(f"Example using your server: python {sys.argv[0]} ../npcpy/work/mcp_server.py get_available_tables '{{\"db_path\": \"~/npcsh_history.db\"}}'")
130+
sys.exit(1)
131+
132+
server_script_path = sys.argv[1]
133+
tool_to_call = sys.argv[2]
134+
args_json_string = sys.argv[3]
135+
136+
try:
137+
tool_arguments = json.loads(args_json_string)
138+
if not isinstance(tool_arguments, dict):
139+
raise ValueError("Arguments must be a JSON object.")
140+
except Exception as e:
141+
print(f"Error parsing arguments JSON: {e}")
142+
sys.exit(1)
143+
144+
print(f"--- Attempting Synchronous MCP Tool Call ---")
145+
print(f"Server: {server_script_path}")
146+
print(f"Tool: {tool_to_call}")
147+
print(f"Args: {tool_arguments}")
148+
print(f"---------------------------------------------")
149+
150+
# Execute with debug=True to see internal logs
151+
result = execute_mcp_tool_sync(server_script_path, tool_to_call, tool_arguments, debug=True)
152+
153+
# --- Result Printing (Remains the same) ---
154+
print("\n--- Result ---")
155+
print(f"Type: {type(result)}")
156+
print("Content:")
157+
if isinstance(result, (dict, list)):
158+
print(json.dumps(result, indent=2))
159+
elif isinstance(result, str):
160+
print(repr(result))
161+
else:
162+
print(result)

0 commit comments

Comments
 (0)