A high-performance, safety-first wrapper for MLX-LM designed for Apple Silicon (M1/M2/M3/M4).
PromptLoop abstracts away the complex logic of running local LLMs — memory management, token streaming, LoRA adapters, MCP tool calling, history trimming, and system safety — allowing you to build powerful, stable AI tools with just a few lines of code.
- Memory Guardian — Active monitoring with configurable RAM/Swap thresholds. If RAM hits 95% or Swap exceeds the limit, the engine stops immediately to prevent system freeze.
- Input Flood Protection — Automatically rejects massive text pastes (e.g., accidental PDFs) that would crash the tokenizer.
- Clean Exit Handling — Built-in signal handlers prevent zombie processes on
Ctrl+C.
- Sampling Profiles — One-click presets (
creative,precise,strict,balanced) to instantly change the model's behavior. - Custom Exit Triggers — Define your own stop words (e.g.,
["bye", "quit", "save"]). - Headless Bridge — Built-in
output_callbackhook to pipe output to GUIs (Tkinter, PyQt, Web) without parsing terminal text.
- Load any LoRA fine-tuned adapter alongside a base model with a single
adapter_pathargument. - Works in both
run_chat()(interactive) andrun_one_shot()(single-turn) modes.
- Give your local LLM access to external tools using the FastMCP standard.
- The engine automatically detects tool calls in the model's output, executes them, and feeds results back into the conversation.
- Bridge any FastMCP server with one function:
mcp_to_promptloop().
- Chat Export — Built-in utility to save conversation history to clean, readable text files.
- Token Timer — Real-time tokens-per-second (TPS) reporting after each generation.
This library is designed for local development on Apple Silicon. Install with uv:
git clone https://github.com/alexspecter/promptloop.git
cd promptloop
uv syncOr install in editable mode with pip:
pip install -e .Requirements: Python ≥ 3.10, macOS with Apple Silicon.
This script demonstrates every core capability of the library: profiles, safety, file saving, custom inputs, and LoRA adapters.
import os
from promptloop import (
run_chat,
get_multiline_input,
configure_input,
register_signal_handlers
)
from promptloop.storage import save_chat_history
def main():
# 1. Clean Exit Handling (Prevents zombie processes)
register_signal_handlers()
# 2. Setup & Configuration
model_path = "mlx-community/Meta-Llama-3.1-8B-Instruct-4bit"
# 3. Custom Input Tool
# Allows pasting multi-line text. Sends when user types '!!!' on a new line.
user_tool = configure_input(
get_multiline_input,
"\n📝 Paste text (Type '!!!' to send): ",
sentinel="!!!"
)
print(f"🔄 Initializing {model_path}...")
# 4. The Engine
try:
# Returns the full chat history when the session ends
history = run_chat(
system_prompt="You are a helpful assistant.",
model_path=model_path,
input_fn=user_tool,
# --- Personality ---
profile="creative", # Options: balanced, creative, precise, strict
# --- LoRA (Optional) ---
adapter_path=None, # Path to a LoRA adapter directory
# --- Safety ---
use_guardian=True, # Kill if RAM > 95%
max_input_tokens=4000, # Reject massive inputs
# --- Control ---
exit_keywords=["bye", "save", "quit"],
stream=True,
# --- UI Polish ---
wait_message="\n(🧠 Thinking...)",
response_prefix="\n🤖 Llama: "
)
# 5. Save Workflow
if history:
save = input("\n💾 Save session? (y/n): ")
if save.lower() == 'y':
save_chat_history(history, filename="~/Desktop/session.txt")
print("✅ Saved to Desktop.")
except Exception as e:
print(f"\n[!] Critical Error: {e}")
if __name__ == "__main__":
main()Give your local model access to external tools using the Model Context Protocol:
from mcp import FastMCP
from promptloop import run_chat, register_signal_handlers
from promptloop.mcp_tools import mcp_to_promptloop
# 1. Define your tools
mcp = FastMCP("MyTools")
@mcp.tool()
def get_weather(city: str) -> str:
"""Get the current weather for a city."""
return f"The weather in {city} is 22°C and sunny."
@mcp.tool()
def search_docs(query: str) -> str:
"""Search internal documentation."""
return f"Found 3 results for '{query}'."
# 2. Bridge to PromptLoop
tools, handler = mcp_to_promptloop(mcp)
# 3. Run with tools
register_signal_handlers()
run_chat(
system_prompt="You are an assistant with access to tools. Use them when needed.",
model_path="mlx-community/Meta-Llama-3.1-8B-Instruct-4bit",
input_fn=input,
tools=tools,
tool_handler=handler,
stream=True,
)When the model decides to call a tool, the engine will:
- Parse the tool call JSON from the model's output.
- Execute the matching tool function.
- Inject the result back into the conversation.
- Re-prompt the model for a final answer.
For single-turn inference (no chat loop), use run_one_shot():
from promptloop import run_one_shot
response = run_one_shot(
model_path="mlx-community/Meta-Llama-3.1-8B-Instruct-4bit",
system_prompt="You are a code reviewer.",
user_prompt="Review this function: def add(a, b): return a + b",
adapter_path=None, # Optional LoRA adapter
)
print(response)Change the model's behavior with a single string:
| Profile | Temp | Top-P | Best For |
|---|---|---|---|
balanced |
0.6 | 1.0 | Default. Good mix of coherence and variety. |
creative |
0.8 | 0.9 | Storytelling. More human-like, varied prose. |
precise |
0.2 | 0.95 | Editing. Stick to facts/instructions. Less hallucination. |
strict |
0.0 | 1.0 | Coding/Math. Deterministic. Always the same answer. |
Override any individual setting while keeping the rest of a profile:
run_chat(
...,
profile="creative", # Standard Top-P: 0.9
temp=1.2 # Override to a much hotter 1.2
)Or bypass profiles entirely with full manual control:
run_chat(
...,
temp=0.75,
top_p=0.92,
top_k=50
)Priority: Manual arguments → Profile values → balanced defaults.
PromptLoop is headless — it can power a GUI without blocking the interface:
import threading
from queue import Queue
gui_queue = Queue()
def start_engine():
run_chat(
...,
input_fn=gui_queue.get, # Wait for GUI input
output_callback=my_text_widget.insert # Send text to GUI
)
threading.Thread(target=start_engine, daemon=True).start()- Input: Use a
Queueto pass text from the GUI to the engine. - Output: Use
output_callbackto pipe text from the engine to your window. - Threading: Run
run_chatin a separate thread.
The main interactive chat engine.
| Parameter | Type | Default | Description |
|---|---|---|---|
system_prompt |
str or dict |
— | System prompt (auto-wrapped if string). |
model_path |
str |
— | HuggingFace repo or local model path. |
input_fn |
Callable |
— | Function that returns user input. |
max_tokens |
int |
2048 |
Max tokens per response. |
history_limit |
int |
10 |
Max conversation turns to keep. |
stream |
bool |
True |
Stream tokens in real-time. |
profile |
str |
"balanced" |
Sampling preset. |
temp |
float or None |
None |
Manual temperature override. |
top_p |
float or None |
None |
Manual top-p override. |
top_k |
int or None |
None |
Manual top-k override. |
adapter_path |
str or None |
None |
Path to LoRA adapter directory. |
tools |
list or None |
None |
MCP tool schemas for the model. |
tool_handler |
Callable or None |
None |
Function to dispatch tool calls. |
use_guardian |
bool |
False |
Enable Memory Guardian. |
max_input_tokens |
int |
4000 |
Max input length before rejection. |
exit_keywords |
list[str] |
["exit","quit"] |
Words that end the session. |
wait_message |
str |
"" |
Message shown while model generates. |
response_prefix |
str |
"" |
Prefix before each response. |
output_callback |
Callable or None |
None |
Redirect output to a function (GUI hook). |
verbose |
bool |
True |
Print status messages. |
Returns: List[Dict[str, str]] — The full message history.
Single-turn inference. No chat loop.
| Parameter | Type | Default | Description |
|---|---|---|---|
model_path |
str |
— | HuggingFace repo or local path. |
system_prompt |
str or dict |
— | System prompt. |
user_prompt |
str |
— | The user's input. |
max_tokens |
int |
2048 |
Max tokens. |
adapter_path |
str or None |
None |
Path to LoRA adapter. |
model |
Any or None |
None |
Pre-loaded model (avoids reload). |
tokenizer |
Any or None |
None |
Pre-loaded tokenizer. |
Returns: str — The model's response.
Bridges a FastMCP server into PromptLoop.
Returns: (tools, tool_handler) — A tuple ready to pass into run_chat().
Exports a message history list to a formatted text file.
Pre-configures an input function with baked-in arguments. Attaches the sentinel as a hidden tag for the engine to auto-discover.
Collects multi-line input until the sentinel string is entered on a new line.
Registers SIGINT handlers for clean Ctrl+C exit behavior.
This project follows the Ironclad Security Protocol:
- All dependencies are version-pinned in
pyproject.tomland locked inuv.lock. - No
eval(),exec(), orshell=Trueanywhere in the codebase. - Pre-commit hooks enforce Ruff, Gitleaks, Trivy, and Semgrep on every commit.
MIT