# Persona Forge

> Fine-tune persona chatbots that never break character — synthetic roleplay data with judge filtering, DPO against assistant boilerplate, QLoRA recipes, and a local RAG loremaster over MCP

- **Type:** MCP server
- **Install:** `agentstack add mcp-ctkadvisors-persona-forge`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [ctkadvisors](https://agentstack.voostack.com/s/ctkadvisors)
- **Installs:** 0
- **Category:** [AI & ML](https://agentstack.voostack.com/c/ai-and-ml)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [ctkadvisors](https://github.com/ctkadvisors)
- **Source:** https://github.com/ctkadvisors/persona-forge

## Install

```sh
agentstack add mcp-ctkadvisors-persona-forge
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# persona-forge

Fine-tune a persona chatbot that **never breaks character** — and let it
search your own books mid-chat with a local RAG "loremaster."

You bring three things: a **corpus** (plain text you have the rights to use —
public-domain classics, your own novel, campaign notes), a **base chat model**,
and any **OpenAI-compatible endpoint** to act as the data-generation teacher
(a local llama.cpp/vLLM/LM Studio server works fine). Everything else is here.

## Quickstart

```bash
pip install -e ".[data,train]"

# 1. Drop your corpus in:
cp /path/to/your/books.txt data/corpus.txt

# 2. Make your persona pack (start from the Arthurian example):
cp packs/camelot.json packs/mine.json   # then edit — see "Customizing" below

# 3. Generate the training blend (points at your teacher endpoint):
PACK=packs/mine.json CORPUS=data/corpus.txt \
OPENAI_BASE_URL=http://localhost:1234/v1 TEACHER=my-teacher-model \
python -m personaforge.build_data

# 4. Train — continued-pretraining on your corpus, then SFT, then DPO,
#    as one chained QLoRA adapter on a single GPU:
MODEL_ID=Qwen/Qwen3.6-27B DATA_DIR=out/data DO_CPT=1 CORPUS=data/corpus.txt \
python -m personaforge.train_run

# 5. Prove it holds character (bare assignments, provocations, style probe):
MODEL_ID=Qwen/Qwen3.6-27B ADAPTER=out/adapter PACK=packs/mine.json \
python -m personaforge.battery

# 6. Get numbers (held-out eval -> out/eval.json):
MODEL_ID=Qwen/Qwen3.6-27B ADAPTER=out/adapter PACK=packs/mine.json \
OPENAI_BASE_URL=http://localhost:1234/v1 JUDGE=my-teacher-model \
python -m personaforge.eval_run
```

The adapter in `out/adapter` is standard PEFT: `merge_and_unload()` it and
convert to GGUF or MLX for serving with your stack of choice.

## Give it a loremaster (RAG over your corpus, via MCP)

Persona models hallucinate lore confidently; retrieval fixes what fine-tuning
can't. Build a local index, then register the MCP server in any MCP host
(LM Studio, Claude Code, ...) so the model can look passages up mid-chat:

```bash
uv run personaforge/rag/build_index.py data/corpus.txt data/index

# in your MCP host's config:
#   command: uv
#   args: ["run", "/path/to/personaforge/rag/mcp_server.py"]
#   env:  LORE_INDEX_DIR=/path/to/data/index
#         LORE_DESCRIPTION="the collected Arthurian romances"
```

Models with native tool training keep that ability through the persona tune —
they'll pick `lore_search` for lore questions and narrate the results in voice.

## Customizing: the persona pack

All world flavor lives in one JSON file; the pipeline is world-agnostic.
Edit these fields in your copy of `packs/camelot.json`:

| Field | What it does |
|---|---|
| `world` | Spliced into every prompt: "in the voice of *{world}*" |
| `cards` | Your characters: `name`, `persona`, `style`, `pronoun` |
| `scenarios` | Situation seeds for generated conversations |
| `provocations` | Rude/crude/meta user turns your model must survive in character |
| `assignments` | Casual persona-assignment phrasings ("be {name}", lowercase happens) |
| `boilerplate` | The assistant-speak to train *away* from (used as DPO rejected) |
| `exemplars` | A few hand-written dialogues that set the voice ceiling |

Knobs on `build_data`: `N_CONVOS` / `N_DPO` / `N_ASSIGN` / `N_ASSIGN_DPO`
(volumes), `IN_DIR` (extend an existing blend instead of building fresh).
Keep roleplay at 25–35% of the final SFT mix — the generated general-chat
blend does this for you at the defaults.

## Evals: retrain and swap models with confidence

`build_data` reserves every 4th provocation/assignment seed — training never
sees them. `eval_run` generates replies to those held-out prompts and scores:

- **assignment_accuracy** — bare "be {name}" answered by the *named* character
  and no other (string check, free, objective)
- **boilerplate_rate** — assistant-speak regex over provocation replies (free)
- **in_character_mean / voice_mean** — judge-scored, needs a teacher endpoint
  (omit `JUDGE` to skip; the free metrics still run)

One JSON report per adapter, so comparing a retrain — or a different base
model against the same `DATA_DIR` — is a diff, not a vibe. The battery
(`personaforge.battery`) stays for eyeballing transcripts; the eval is what
you gate a release on.

## Why the weird data? (the two failure modes)

Naive persona tunes fail two ways, both reproducible: (1) **the boilerplate
break** — the user gets rude or meta and the model drops the persona for "I am
a professional and do not engage in such language"; (2) **the wrong-character
pickup** — a bare "hey, you're merlin. roleplay as him" gets answered by a
*different* character introducing themself. Both are data gaps. The pipeline
generates conversations that survive provocation, assignments answered by the
named character (with a hard wrong-name check), and DPO pairs whose rejected
side is the failure verbatim. In our reference run (27B dense, single
unified-memory GPU) this took the DPO reward margin from 0.003 to 0.45 and a
clean sweep on the battery.

## Recipe notes (hard-won)

- Reuse the CPT adapter across retrains: set `CPT_ADAPTER=out/cpt` and skip
  the expensive knowledge stage.
- Pin `device_map={"": 0}` on unified-memory boxes; `"auto"` triggers
  CPU-offload chaos. (Already done in the trainers.)
- bitsandbytes 4-bit works on ARM/Blackwell for dense models; MoE models may
  crash — set `load_in_4bit=False` in `QLoRAConfig` for bf16 LoRA.
- Thinking-mode base models: eval with thinking disabled, and check your chat
  template's *default* before shipping — some inference stacks force-inject
  `enable_thinking=True`.
- Expect the judge to keep ~75% of generated conversations and ~90% of
  introductions; lower than that usually means a weak teacher model.

## License

MIT. No corpora, weights, or copyrighted-world data ship here — the pipeline
is yours, your data responsibilities are your own.

## Source & license

This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [ctkadvisors](https://github.com/ctkadvisors)
- **Source:** [ctkadvisors/persona-forge](https://github.com/ctkadvisors/persona-forge)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/mcp-ctkadvisors-persona-forge
- Seller: https://agentstack.voostack.com/s/ctkadvisors
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
