Local-first, organic digital-marketing automation re-engineered as a visual Drag-and-Drop Workflow Builder. Drag modular blocks onto a canvas, wire them together, and run them sequentially — powered by a dual-routing SLM fleet with a strict 3000MB RAM ceiling and automatic offload to external GPU servers.
Runs 24/7 on a 16GB Linux partition alongside n8n, SearXNG, and ChromaDB. No cloud tokens required. OOM-safe under any n8n concurrency burst.
OrganicForge is the backend orchestration engine for a visual marketing-workflow builder. You (or an external n8n workflow) define a graph of marketing nodes — SEO Auditor, Content Generator, Live Web Search, Social Poster — and the engine executes them in topological order, passing data down the wires, with every node powered by the right local model for the job.
The engine is built for a two-host fleet architecture:
| Host | RAM | Role | What runs here |
|---|---|---|---|
| Linux partition | 16GB (single stick) | 24/7 automation host | OrganicForge API + 2 local SLMs + n8n + SearXNG + ChromaDB |
| Windows host | 32GB + 6GB VRAM | Heavy compute + GPU offload | kilocode extension server (laguna/mimo/nemotron/ling) + opencode proxy (Gemma-2-9B, DeepSeek-R1-8B) |
The Linux box never loads a model heavier than 2.6GB. Everything else is routed over the LAN to the Windows host's OpenAI-compatible servers — zero local RAM cost during heavy n8n bursts.
| Model | Role | Quant | RAM | Used for |
|---|---|---|---|---|
phi-4-mini (3.8B) |
fast_logic |
Q4_K_M | 2.6GB | SEO audits, URL parsing, structural logic |
Llama-3.2-3B-Instruct |
creative_copywriting |
Q4_K_M | 2.2GB | Social posts, email copywriting |
Only one is resident at a time. The engine unloads (Llama.close() + dual gc.collect()) before loading the next, so peak RSS stays under the 3000MB ceiling.
| Provider | Endpoint | Models → Roles |
|---|---|---|
| kilocode | http://192.168.1.10:1234/v1 |
laguna→creative, mimo→fast_logic, nemotron→strategic_reasoning, ling→json_structured |
| opencode | http://192.168.1.10:8081/v1 |
gemma-2-9b→creative, deepseek-r1-8b→strategic_reasoning |
The json_structured and strategic_reasoning roles have no local GGUF — they always route to the Windows host.
generate() decides local-vs-external in this order:
- Explicit external key (
"kilocode:laguna") → call external directly - Role has no local GGUF (
json_structured,strategic_reasoning) → must route external - n8n burst active (≥3 concurrent requests) → route ALL roles externally to keep Linux RAM flat
prefer_externalflag → route external- Default → load local GGUF (RAM-guarded)
- External failure → fall back to local GGUF if the role has one
The ConcurrencyTracker detects n8n bursts in real time. When ≥3 webhooks hit simultaneously, the engine flips to external-only routing so the Linux box's RAM stays flat — no OOM crashes.
OrganicForge/
├── configs/
│ └── model_config.yaml # the 2-SLM fleet + external providers + RAM guard (3000MB)
├── src/
│ ├── __init__.py
│ ├── engine.py # InferenceEngine v2.2: dual-routing + burst detection + RAM GC
│ ├── searxng_client.py # bulletproof graceful-failure SearXNG client
│ ├── app.py # FastAPI: /api/execute-workflow + /api/v2/n8n-trigger
│ └── modules/
│ ├── __init__.py
│ ├── seo.py # SEO Auditor node (phi-4-mini + SearXNG context)
│ └── content.py # Content Generator node (qwen + Pydantic schema)
├── requirements.txt
└── README.md
pip install -r requirements.txtllama-cpp-python needs a C++ toolchain. For hardware-accelerated builds (CUDA / Metal / OpenBLAS), see: https://github.com/abetlen/llama-cpp-python#installation
# SearXNG (optional but recommended — graceful degradation if absent)
docker run -d --name searxng -p 8080:8080 searxng/searxng
# ChromaDB is auto-initialized by the engine on first query (persisted to .chroma/)
# n8n runs separately and calls into /api/v2/n8n-triggerRun kilocode and opencode on the Windows machine so their OpenAI-compatible
servers are reachable at the IPs in configs/model_config.yaml. (If they're
offline, the engine falls back to the local SLMs — the roles without a local
GGUF will return empty output with a logged error.)
python -m src.app
# or
uvicorn src.app:app --host 0.0.0.0 --port 8000The first run downloads the 2 GGUF files from HuggingFace (~2.4GB + ~2.0GB, cached under ~/.cache/huggingface).
Engine + SearXNG + ChromaDB + external-provider status snapshot.
The configured local SLMs + external model registry + role index.
Available node types + their input schemas (for the visual canvas palette).
Run a visual node-canvas graph (topological sort + per-node execution):
{
"nodes": [
{ "id": "n1", "type": "seo", "data": { "url": "https://example.com", "target_keyword": "organic coffee" } },
{ "id": "n2", "type": "content", "data": { "topic": "organic coffee", "content_type": "social_post" } }
],
"edges": [
{ "id": "e1", "source": "n1", "target": "n2" }
]
}The locked-down endpoint external n8n workflows POST into. Never raises — every exception is caught and returned as {ok: false, error} so a concurrent n8n loop can never crash the primary Python server thread.
POST /api/v2/n8n-trigger
{
"kind": "generate_copy",
"prompt": "Write a social post about {{topic}} for {{audience}}",
"variables": { "topic": "organic coffee", "audience": "millennials" },
"context_query": "organic coffee marketing 2025",
"max_tokens": 512,
"temperature": 0.6
}Response:
{
"ok": true,
"kind": "generate_copy",
"role": "creative_copywriting",
"model": "llama-3.2-3b",
"output": "☕ Your morning deserves better...",
"error": null,
"duration_ms": 2840,
"concurrency": { "active": 1, "burst_threshold": 3, "in_burst": false },
"variables_applied": 2
}Task kinds → roles:
kind |
Role | Routes to |
|---|---|---|
audit_seo |
fast_logic |
phi-4-mini (local) OR kilocode:mimo (external) |
generate_copy |
creative_copywriting |
Llama-3.2-3B (local) OR kilocode:laguna OR opencode:gemma-2-9b |
generate_json |
json_structured |
kilocode:ling (external only — no local GGUF) |
strategy_map |
strategic_reasoning |
kilocode:nemotron OR opencode:deepseek-r1-8b (external only) |
custom |
fast_logic |
phi-4-mini (default) |
- Create
src/modules/<name>.pywith arun(input_data: dict, engine: InferenceEngine) -> dictfunction. Return{"ok": bool, "error": str|None, ...}. - Register it in
src/app.py's_NODE_REGISTRY+_NODE_INPUT_SCHEMAS. - It now appears in
/api/modulesand is executable in the canvas.
All knobs live in configs/model_config.yaml:
- the 2 local SLMs (repo_id, filename, ram_mb, role)
- the external providers (kilocode + opencode base URLs + model→role maps)
- the RAM guard ceiling (
target_total_mb: 3000) - the burst-mode threshold (
burst_threshold: 3) - SearXNG endpoint + categories
- ChromaDB persist dir + collection name
- llama.cpp runtime (threads, GPU layers, context size)
Every prompt can be grounded in two context sources, blended into a preamble:
- Local foundational knowledge — ChromaDB collection embedded with
all-MiniLM-L6-v2. Offline, fast, grounded. - Real-time web context — pulled from the local SearXNG instance.
If SearXNG is offline, the SearXNGClient fails gracefully (never raises) and the engine proceeds with ChromaDB-only context.
MIT — see LICENSE.