From 4a2a00966d67c40b2c4517573f290579c4397f73 Mon Sep 17 00:00:00 2001 From: Carlos Tello Date: Mon, 21 Sep 2026 10:35:44 -0300 Subject: [PATCH] Add local LLM configuration and tool demo --- .gitignore | 2 + litellm_config.yaml | 10 +++++ tool_call_demo.py | 106 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 118 insertions(+) create mode 100644 .gitignore create mode 100644 litellm_config.yaml create mode 100644 tool_call_demo.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e6b3e99 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# Backups locales: pueden contener claves y secretos +ubuntu-config-*.tar.gz diff --git a/litellm_config.yaml b/litellm_config.yaml new file mode 100644 index 0000000..68530c2 --- /dev/null +++ b/litellm_config.yaml @@ -0,0 +1,10 @@ +model_list: + - model_name: qwen2.5-coder:14b + litellm_params: + model: ollama_chat/qwen2.5-coder:14b + api_base: http://127.0.0.1:11434 + max_tokens: 4096 + +litellm_settings: + drop_params: true + json_to_tool_call: true diff --git a/tool_call_demo.py b/tool_call_demo.py new file mode 100644 index 0000000..1c53bd4 --- /dev/null +++ b/tool_call_demo.py @@ -0,0 +1,106 @@ +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))