107 lines
3.5 KiB
Python
107 lines
3.5 KiB
Python
import json
|
|
from pathlib import Path
|
|
|
|
SYSTEM_PROMPT = (
|
|
"You are a helpful assistant. "
|
|
"When outputting Windows file paths in JSON arguments, you must strictly escape all backslashes "
|
|
"(for example: C:\\\\Users\\\\name\\\\file.txt). "
|
|
"If you need to read a file, emit a tool call with the path field."
|
|
)
|
|
|
|
|
|
def read_file(path: str) -> str:
|
|
"""Read a file from disk and return its contents."""
|
|
file_path = Path(path)
|
|
try:
|
|
return file_path.read_text(encoding="utf-8")
|
|
except FileNotFoundError:
|
|
return f"ERROR: File not found: {path}"
|
|
except Exception as exc: # pragma: no cover - demo only
|
|
return f"ERROR: {type(exc).__name__}: {exc}"
|
|
|
|
|
|
def sanitize_tool_arguments(raw_arguments: str):
|
|
"""Repair malformed JSON emitted by the model when Windows paths are not escaped."""
|
|
try:
|
|
return json.loads(raw_arguments)
|
|
except json.JSONDecodeError:
|
|
repaired = raw_arguments.replace("\\", "\\\\")
|
|
return json.loads(repaired)
|
|
|
|
|
|
def handle_tool_call(response: dict) -> dict:
|
|
"""Detect a tool call in the LLM response and execute it locally."""
|
|
tool_calls = response.get("tool_calls") or response.get("function_call")
|
|
|
|
if tool_calls is None:
|
|
return {"status": "final_response", "content": response}
|
|
|
|
if isinstance(tool_calls, dict):
|
|
tool_calls = [tool_calls]
|
|
|
|
for tool_call in tool_calls:
|
|
function_data = tool_call.get("function", tool_call)
|
|
name = function_data.get("name")
|
|
arguments = function_data.get("arguments", {})
|
|
|
|
if isinstance(arguments, str):
|
|
try:
|
|
arguments = json.loads(arguments)
|
|
except json.JSONDecodeError:
|
|
try:
|
|
arguments = sanitize_tool_arguments(arguments)
|
|
except json.JSONDecodeError:
|
|
return {
|
|
"status": "invalid_arguments",
|
|
"raw_arguments": arguments,
|
|
"message": "The model emitted malformed JSON. Ensure backslashes are escaped.",
|
|
}
|
|
|
|
if name == "read_file":
|
|
file_path = arguments.get("path")
|
|
content = read_file(file_path)
|
|
return {
|
|
"status": "tool_result",
|
|
"tool_call_id": tool_call.get("id"),
|
|
"content": content,
|
|
}
|
|
|
|
return {"status": "unsupported_tool_call", "raw": response}
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# A valid OpenAI-style tool call with a Windows path escaped correctly.
|
|
valid_response = {
|
|
"tool_calls": [
|
|
{
|
|
"id": "call_read_001",
|
|
"type": "function",
|
|
"function": {
|
|
"name": "read_file",
|
|
"arguments": '{"path": "c:\\Workspace\\llm-server-setup\\README.md"}',
|
|
},
|
|
}
|
|
]
|
|
}
|
|
|
|
# This reproduces the common bug: malformed Windows path in JSON.
|
|
broken_response = {
|
|
"tool_calls": [
|
|
{
|
|
"id": "call_read_002",
|
|
"type": "function",
|
|
"function": {
|
|
"name": "read_file",
|
|
"arguments": '{"path": "c:\Workspace\llm-server-setup\README.md"}',
|
|
},
|
|
}
|
|
]
|
|
}
|
|
|
|
result = handle_tool_call(valid_response)
|
|
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
|
|
print("\n--- malformed-path fallback sample ---")
|
|
fallback = handle_tool_call(broken_response)
|
|
print(json.dumps(fallback, ensure_ascii=False, indent=2))
|