WP Manifestindependent plugin directory
manifest / unclassified / mxchat-plus

MxChat Plus

Companion plugin for MxChat: DuckDB/MotherDuck vector store, Anthropic prompt caching, and CSV transcript export — in one plugin, three switchable modules.

by Paul Argoud · github.com/paulargoud/mxchat-plus · website

★ 0stars
0forks

Install

No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:

wp plugin install https://github.com/paulargoud/mxchat-plus/archive/refs/heads/main.zip

Plugin version PHP WordPress MxChat DuckDB License: GPL v2+ CI

One companion plugin, four modules, for the third-party MxChat chatbot plugin (mxchat-basic) — a DuckDB / MotherDuck vector store that replaces Pinecone, Anthropic prompt caching that cuts input-token cost on Claude calls, a CSV export of the transcripts you actually selected, and Matomo / GA4 events for the links and suggested questions visitors click. It hooks MxChat entirely through WordPress filters and never modifies a single file of the host plugin.


Read this first — two silent-failure prerequisites

The vector-store and prompt-cache modules degrade silently when their prerequisite is missing: nothing errors, nothing is logged, the feature simply never engages. Check these before filing a bug. (The transcripts module has no such prerequisite. The tracking module has one, and it fails the same quiet way: events go nowhere unless a Matomo or GA4 tag is already loaded on the page — see docs/tracking/USAGE.md.)

(a) MxChat streaming must be OFF for prompt caching to do anything

MxChat streams Claude responses with raw curl_exec(), not wp_remote_post() — 8 occurrences in mxchat-basic/includes/class-mxchat-integrator.php on version 3.2.21, including the one inside mxchat_generate_response_claude_stream() (line 11444). That bypasses the WordPress HTTP API entirely, so the http_request_args filter this module relies on is never invoked and no cache_control breakpoint can be injected.

Action required: turn streaming off in MxChat → Settings (the enable_streaming_toggle key inside the host's mxchat_options). MxChat then falls back to mxchat_generate_response_claude() (line 12411), which goes through wp_remote_post() and is fully interceptable.

Trade-off: you lose the typewriter effect; you gain ~85–95 % input-token cost reduction once the cache is warm (typically 3–5 requests).

Cacheable even with streaming ON: the admin Content Generator, the admin "Test API" one-shot, and the non-streaming error fallback — all three use wp_remote_post(). Not cacheable: the main chat while streaming, and the admin "Test streaming" button.

(b) The Pinecone proxy needs a working HTTPS loopback and pretty permalinks

The DuckDB module's default integration (Option B) impersonates Pinecone over REST: MxChat is told its "Pinecone host" is this site, and it calls back into /wp-json/mxchat-plus/v1/…. Three things must hold, or every retrieval silently returns zero matches:

  1. HTTPS must work, including loopback. MxChat hardcodes https:// when building the Pinecone URL. A self-signed certificate, an HTTP-only site, or a host that blocks the server calling its own domain all break the round-trip.
  2. Pretty permalinks must be on. The plain ?rest_route= form is not what MxChat constructs; Settings → Permalinks must be anything other than "Plain".
  3. The REST namespace must be reachable — some security plugins and WAFs block /wp-json/ wholesale. curl -I https://your-site/wp-json/mxchat-plus/v1/health from the server itself is the one-line check.

A third thing worth knowing: tools/patches/ targets a filter that does not exist

tools/patches/mxchat-pre-vector-query.diff adds an mxchat_pre_vector_query filter to find_relevant_content_pinecone() so the DuckDB module can short-circuit the HTTP hop (Option A). That filter is not present in mxchat-basic 3.2.21 — verified against all 53 hooks the host exposes. The patch is a proposal for upstream, it is optional and inert by default, and applying it means modifying the host plugin, which this project otherwise never does (and which an MxChat update will revert).

The nominal, supported path is the REST proxy (Option B). It needs no patch.


The four modules

Module What it does Where it lives
DuckDB / MotherDuck vector store Stores MxChat's vector knowledge base in an embedded .duckdb file or in MotherDuck cloud, with HNSW-indexed similarity search, hybrid BM25 + vector retrieval, a query cache, Parquet export/import and a Pinecone → DuckDB migrator. Presents itself to MxChat as a Pinecone endpoint. includes/duckdb/, admin/views/duckdb/, docs/duckdb/
Prompt caching Injects Anthropic cache_control breakpoints (tools, system, and a rolling pair on the last two user messages) into outbound Claude requests, and measures cached tokens across Anthropic, OpenAI, OpenRouter, xAI, DeepSeek and Gemini. includes/promptcache/, docs/promptcache/USAGE.md
Transcripts CSV export Adds an export button to MxChat's transcripts screen: exports the conversations you ticked, or — with nothing ticked — every conversation between two dates, through a modal styled like the host's own. UTF-8 BOM for Excel, CSV-injection neutralised, 2000 conversations per export. includes/transcripts/, docs/transcripts/USAGE.md
Click tracking Sends a Matomo (and optionally GA4) event when a visitor clicks a link inside a bot answer or a suggested question, internal and external distinguished, with the conversation's session id attached. Adds an admin report over the clicks MxChat already logs server-side but never aggregates. Off by default — the only module that puts code on public pages. includes/tracking/, admin/views/tracking/, docs/tracking/USAGE.md

The modules are independent: each can be left idle without affecting the others. They share only the plugin bootstrap, the mxchat_plus_* option namespace, the mxchat-plus text domain and the wp mxchat-plus CLI root.

Why a vector store

MxChat ships two backends: MySQL (embeddings in LONGTEXT, cosine similarity computed in PHP — simple, slow past a few thousand rows) and Pinecone (fast, managed, proprietary, per-record pricing). This adds a third: DuckDB / MotherDuck — an analytical columnar database with a native VSS extension. Open source, $0 for embedded mode.

Why prompt caching

MxChat resends a large system prompt and the full conversation history on every turn. Anthropic's prompt caching reuses identical prefixes at roughly 0.1× the input-token price, and cuts time-to-first-token. The module places up to the 4 breakpoints Anthropic allows, using a rolling pair on the conversation so the write breakpoint of turn N becomes the read breakpoint of turn N+1 — the hit rate stays flat as the conversation grows.

Why a transcripts export

MxChat already ships wp_ajax_mxchat_export_transcripts, but it dumps the whole wp_mxchat_chat_transcripts table unconditionally: the checkboxes on its own transcripts screen have no effect on it, and there is no date filter. This module exports exactly what was asked for — the ticked conversations, or a date range — under its own AJAX action and its own nonce. The host renders that toolbar itself and exposes no hook to render into, so the button is injected client-side and re-injected after each list re-render; the selection is read back from the checkboxes, because the host keeps its selectedSessions Set private to its own closure.

Why click tracking

MxChat 3.2.21 does log link clicks server-side, into its own wp_mxchat_url_clicks table — but only for absolute http(s) links (js/chat-script.js:2188), so a relative link the assistant writes is never recorded; it does not record internal vs external; it does not track suggested questions at all (its .mxchat-popular-question handler just sends the question); and it surfaces the result as nothing more than a DISTINCT clicked_url list per conversation, absent from its CSV export and its REST API.

This module covers both halves of that gap. The browser half sends the events to the analytics stack the site already runs — which is the only way to see what a visitor does after leaving for the link, something no WordPress table can tell you. The admin half turns the host's own table into an actual report, with counts and an export.

The two count different populations and will not match: ad-blockers suppress the Matomo events but never the host's table, and the host's table misses every relative link and every suggestion. That is by construction, not a bug.


Requirements

Component Version
PHP ≥ 8.1
WordPress ≥ 6.0
MxChat (mxchat-basic) ≥ 3.2.21
Site protocol HTTPS with working loopback (DuckDB module, Option B)
Permalinks Pretty (not "Plain") — DuckDB module, Option B
DuckDB runtime PECL duckdb extension (preferred) or the duckdb CLI binary
MotherDuck (optional) A token from app.motherduck.com
Prompt caching MxChat streaming disabled (see above)

There are no Composer runtime dependencies. composer.json exists purely for the dev toolchain (PHPUnit, PHPStan); the release zip ships no vendor/ directory and the plugin boots through its own require_once chain.

Installation

cd wp-content/plugins/
git clone https://github.com/paulargoud/mxchat-plus.git

Activate MxChat Plus in the WordPress plugins screen, after MxChat itself. Or download mxchat-plus-x.y.z.zip from Releases and use Plugins → Add New → Upload Plugin.

Quick start

DuckDB module

  1. MxChat → DuckDB / MotherDuck in the admin.
  2. Pick a backend — MotherDuck (token + database name) or Embedded (leave the path empty for the default under wp-content/uploads/mxchat-plus-private/, protected by an auto-written .htaccess + index.php + web.config).
  3. Test connection.
  4. Ingest: Sync MySQL → DuckDB if MxChat already has embeddings in wp_mxchat_system_prompt_content, or Reprocess all posts if you are coming from Pinecone-only. Reprocessing calls the embedding API configured in MxChat and costs real money (typically a few cents for 100–500 posts).
  5. Verify: wp mxchat-plus duckdb stats.

Prompt-cache module

  1. Turn off streaming in MxChat settings (see the prerequisite above).
  2. Nothing else to configure.
  3. After a handful of chat turns: wp mxchat-plus promptcache stats, or read the dashboard widget.

Transcripts module

  1. Nothing to configure — it is on by default in MxChat Plus → Modules.
  2. Open admin.php?page=mxchat-transcripts (MxChat's transcripts screen).
  3. Tick some conversations and click the download icon next to Delete Selected to export those; click it with nothing ticked to pick a date range instead.

Click tracking module

  1. Enable it in MxChat Plus → Modules — it ships off, unlike the other three.
  2. Open the Click tracking tab and switch on Matomo, GA4, or both. Nothing is sent, and no script is even enqueued, while both are off.
  3. Matomo needs no site id here: events ride the tracker already on the page. Optionally create an Action-scope custom dimension in Matomo and enter its number to receive the session id there rather than appended to the event name.
  4. Tick Debug temporarily and watch the DevTools console for [MxChat Plus tracking] lines — then untick it; it logs for every visitor, not just administrators.

Relationship to the host plugin

mxchat-basic is a third-party plugin and is never modified. Everything happens through WordPress hooks: http_request_args / http_response for prompt caching, MxChat's own Pinecone-configuration filters plus a REST endpoint for the vector store, admin_enqueue_scripts plus an own-namespace wp_ajax_ action for the transcripts export (whose button is injected client-side, because the host's toolbar offers no hook), and wp_enqueue_scripts for the click tracker — which observes the host's widget from a document-level listener in the capture phase, because the host binds its own handler directly on each <a> and calls stopPropagation(), and reads the bot id and session id through the globals MxChat itself exports for add-ons.

Consequently the host's own symbols are used verbatim, never renamed: mxchat_options, mxchat_active_embedding_model, mxchat_system_prompt_content, mxchat_embedding_chunk_meta, mxchat_vectors, mxchat_kb_*, mxchat_pinecone_*, mxchat_get_bot_options, mxchat_get_bot_pinecone_config, the wp_ajax_mxchat_* actions and their mxchat_*_nonce nonces. Everything this plugin owns is prefixed mxchat_plus_ / MXCHAT_PLUS_ / MxChat_Plus_. If you see an unprefixed mxchat_ symbol in this codebase, it belongs to the host and must stay exactly as it is.

Documentation

Doc What's in it
ARCHITECTURE.md Start here. Boot sequence, the four modules, and the real integration surface of MxChat 3.2.21 — which hooks exist, which don't, and the traps.
docs/duckdb/ARCHITECTURE.md How the vector store wires into MxChat, query lifecycle, file layout, design conventions.
docs/duckdb/CONFIGURATION.md Every option, sidecar options, where data lives, dimension/storage change guards.
docs/duckdb/HOOKS.md Filters and actions the DuckDB module exposes.
docs/duckdb/CLI.md wp mxchat-plus duckdb … reference with sample output.
docs/duckdb/USAGE.md Async reprocess, Pinecone migration, Parquet backup/restore, INT8 quantization, /health.
docs/duckdb/MIRROR.md Local mirror for MotherDuck installs: when to enable, status states, troubleshooting.
docs/duckdb/BACKUP.md Backup + restore workflow and disaster-recovery checklist.
docs/promptcache/USAGE.md Breakpoint strategy, per-model thresholds, filters, CLI, reading the hit rate.
docs/transcripts/USAGE.md The two export modes, the exact CSV format, the 2000-conversation cap, security, troubleshooting.
docs/tracking/USAGE.md Matomo and GA4 setup, the event names, the overlap with MxChat's own click table, privacy, troubleshooting.
tools/patches/README.md The optional, currently-inapplicable upstream patch (Option A).
CHANGELOG.md Release history, including both pre-merge histories.
CONTRIBUTING.md Filing bugs, sending PRs, running the test suite.
SECURITY.md Reporting a vulnerability, scope, hardening defaults.

Limitations

Vector store

  • Shared hosting: the PECL duckdb extension is rarely available; the CLI fallback uses proc_open(), which some hosts disable. CLI mode adds ~50–200 ms per query.
  • MotherDuck + CLI: every query re-runs ATTACH 'md:…' (1–3 s handshake). Install the PECL extension for production traffic.
  • MotherDuck cloud has no VSS extension, so HNSW is unavailable there. Enable the local mirror (recommended past ~100k vectors) or use the embedded backend; otherwise queries are brute-force scans. The admin surfaces a notice in that combination.
  • HNSW + bot_id filter: DuckDB VSS does not push arbitrary WHERE clauses into the index, so multi-tenant queries fall back to a scan.
  • Embedding dimension must match MxChat's active model; changes are blocked once the table holds vectors (wipe and re-sync to switch).
  • Direct SQL writes to wp_mxchat_system_prompt_content outside MxChat's UI only propagate at the next incremental cron tick.

Prompt caching

  • Streaming is not interceptable — see prerequisite (a).
  • OpenRouter cannot be injected into, by construction: MxChat concatenates the RAG context into the system message there, putting volatile content at the head of the prefix and defeating prefix matching. OpenRouter is measured, never injected.
  • Multi-provider measurement is non-streamed only (wp_remote_post responses); their streamed cURL responses are invisible.
  • Counters are best-effort under concurrency: read-modify-write without a lock, so heavy parallel traffic can slightly undercount (statistics only — the cache is unaffected).
  • A dynamic system prompt kills the cache. A shortcode or filter injecting the time, stock levels or {visitor_name} into the system prompt invalidates the prefix every turn; stats flags this as "cache writes > reads".

Transcripts export

  • 2000 conversations per export. A larger selection is truncated to the first 2000 rather than rejected, and the truncation is silent — split the work into several exports.
  • The button is injected into markup this plugin does not own. It anchors on the host's #mxch-delete-selected element; if a future MxChat release renames it, the button simply stops appearing (no error, nothing logged).
  • Reading wp_mxchat_chat_transcripts directly is the one accepted coupling to the host's storage — it is the same schema the host's own export reads.
  • One synchronous request, no progress indicator. A wide date range on a busy install can produce a very large file.

Click tracking

  • It hooks selectors and globals the host does not guarantee. .bot-message, .mxchat-popular-question, window.getBotIdFromElement and window.getChatSession are MxChat internals. They all exist in 3.2.21 and the globals are exported "for add-ons", but none is a documented contract: a rename in a future release stops the events with no error anywhere. Turn on Debug to check after a host update.
  • Ad-blockers suppress it entirely. Matomo and GA4 are both commonly blocked; the events simply never leave. The admin report, which reads the host's server-side table, is unaffected — which is exactly why the two never reconcile.
  • The first click can carry no session id. MxChat creates the session lazily — on the first message sent, or on widget open only when chat persistence is on. A click that precedes that reports an empty session id rather than a wrong one.
  • No consent management. The only signal the host exposes is mxchatChat.complianz_toggle, and it reflects the marketing category, not statistics. Nothing is wired to it: if your site needs consent gating, gate the Matomo/GA4 tag itself.
  • send_session_id sends a join key off-site. It links an analytics event to a row in the host's transcripts table, and an erasure request handled by WordPress does not reach Matomo or GA4. Turning it off is the conservative choice.
  • The report only counts absolute http(s) links. That is the host's table's own limitation, not ours — it never logged relative links or suggested questions.

Contributing

See CONTRIBUTING.md. TL;DR: PHP 8.1+, composer lint, composer test, composer stan, one ## [Unreleased] entry per user-visible change, and every user-facing string through __() with the mxchat-plus domain — then re-run msgfmt and commit the .mo.

License

GPLv2 or later, same as MxChat itself.

Acknowledgements