Get Name — addressee/given-name extraction for cactus-needle
get-name is a small LoRA fine-tune of the cactus-needle base model
(Cactus-Compute/needle2) that extracts the given (first) name of the
intended person in a piece of text.
It was trained to answer one question: "which single person is this text about / addressed to, and what is that person's first name?" — and to call a tool only when such a person exists.
| Input | Output |
|---|---|
Nice to meet you, Alex |
Alex |
By the way, Alice, if you don't know, how old am I? |
Alice |
The birth certificate was issued to John Snow |
John |
Right now, there are me, Carl, Stephanie, and you, Greg, in the room. |
Greg |
Please let Maria know about the meeting. |
Maria |
Give this package to Lucas. |
Lucas |
Hi everyone, thanks for coming. |
(no call) |
My brother Carl lives in Boston. |
(no call — mention, not target) |
Behaviour
- Given name only. Full names are collapsed to the first name on purpose
(
John Snow→John,James Miller→James). - Single target. In enumerations the person directly addressed
(
you, Greg) is preferred over names merely listed (the team was Carl, Stephanie, and Greg→ no call). - No call when there is no target: group greetings, place names, brands, assistants (Siri/Alexa), generic salutations, and passive mentions.
How to use
The archive is a needle .cact weights file for cactus-needle.
# pull the archive
needle download Qrzysztof/get-name
from needle import Needle
tool = {
"name": "extract_name",
"parameters": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The given (first) name of the intended person; never the surname.",
}
},
"required": ["name"],
},
}
system = (
"You extract the given (first) name of the intended person. "
"If the text has no intended person, do not call the tool."
)
agent = Needle(tools=[tool], weights="tuned.cact", system=system)
print(agent.complete("Nice to meet you, Alex"))
# {"name": "Alex"}
Note: the
systemprompt above is mandatory for best results — the model was trained with it in every example and the same string must be passed at inference.
Running in the browser (no server)
The needle engine ships an official WebAssembly build, so this model runs entirely in the browser — no backend, no Python. Only three assets are needed:
| Asset | Source |
|---|---|
needle.js (62 KB, Emscripten glue) |
Cactus-Compute/needle2/wasm/needle.js |
needle.wasm (325 KB, engine) |
Cactus-Compute/needle2/wasm/needle.wasm |
tuned.cact (13.7 MB, this model) |
this repo |
Everything is fetchable from the Hugging Face CDN (CORS-enabled), so a purely
static page works. Open the included browser-demo.html
to try it, or use the live Space: https://huggingface.co/spaces/Qrzysztof/get-name-demo
(direct: qrzysztof-get-name-demo.static.hf.space), or wire up the same calls
yourself:
<script src="https://huggingface.co/Cactus-Compute/needle2/resolve/main/wasm/needle.js"></script>
<script>
const SYSTEM = "You extract the given (first) name of the intended person. If the text has no intended person, do not call the tool.";
const TOOLS = [{ name: "extract_name", parameters: { type: "object", properties: { name: { type: "string" } }, required: ["name"] } }];
let mod;
const enc = s => { const b = new TextEncoder().encode(s + "\0"); const p = mod._malloc(b.length); mod.HEAPU8.set(b, p); return p; };
async function init() {
const [wasm, cact] = await Promise.all([
fetch("https://huggingface.co/Cactus-Compute/needle2/resolve/main/wasm/needle.wasm").then(r => r.arrayBuffer()),
fetch("https://huggingface.co/Qrzysztof/get-name/resolve/main/tuned.cact").then(r => r.arrayBuffer()),
]);
mod = await createNeedle({ wasmBinary: wasm });
const p = mod._malloc(cact.byteLength);
mod.HEAPU8.set(new Uint8Array(cact), p);
if (mod._needle_load(p, BigInt(cact.byteLength)) !== 0) throw new Error("needle_load failed");
mod._free(p);
if (mod._needle_init(enc(SYSTEM), enc(JSON.stringify(TOOLS)), 0) < 0) throw new Error("needle_init failed");
}
function complete(text) {
const qP = enc(text), cap = 1 << 20, outP = mod._malloc(cap);
const rc = mod._needle_complete(qP, 512, outP, cap);
const bytes = mod.HEAPU8.subarray(outP, outP + Math.min(rc, cap));
let end = bytes.indexOf(0); if (end < 0) end = bytes.length;
const response = JSON.parse(new TextDecoder().decode(bytes.subarray(0, end)));
mod._free(qP); mod._free(outP);
return response; // { type, function_calls: [{ name, arguments: { name } }], … }
}
await init();
console.log(complete("Nice to meet you, Alex")); // function_calls[0].arguments.name === "Alex"
console.log(complete("Hi everyone, thanks for coming.")); // function_calls === []
</script>
Browser specifics (verified against the WASM build):
- API is identical to the native engine —
needle_load/needle_init/needle_complete/needle_reset. Return codes follow the Python binding:needle_loadreturns0on success;needle_initandneedle_completereturn negative on error. needle_loadtakes the cact length as a 64-bitBigInt, andtoolsmust be a JSON array (like the Python binding sends).- Hosting: serve statically (GitHub Pages, Vercel, or just
python3 -m http.server) — HF's CDN allows the cross-origin fetches. - Footprint: ~13.7 MB download, ~325 KB WASM, and ~150–200 MB process memory
(measured via Node RSS while loading weights + running a completion; not the
engine's
peak_ram_mbfield, which proved unreliable in this build). Roughly ~50 tokens/s on recent Apple Silicon / desktop Chrome (2-bit/4-bit quantized weights). - One call per
complete(); for batch processing run a fresh engine instance per text (same guidance as the Python API in Known limitations).
Training
- Method: LoRA adapters (rank 16, alpha 32) on the five attention
projections of every layer of a frozen
Cactus-Compute/needle2base; engine, tokenizer and confidence head untouched. Adapter merged into the weights at export. - Data: ~1,100 hand-curated, template-augmented examples in needle's
tool-calling JSONL format (
data.jsonlin this repo), 88 % positive / 12 % negative, covering:- greetings & welcomes,
- vocative / direct address (incl. titled forms,
Dr. John Snow, …), - documents issued to a person (birth certificates, passports, licences…),
- enumerations with an addressed member (
…and you, Greg,…), - message targets and relays (
Tell Maria…,Pass this note to Owen…), - off-topic no-call examples (groups, places, brands, assistants, generic salutations).
- Hyper-parameters: batch 16, lr 1e-4 with warmup + cosine decay, grad clip 1.0, epochs chosen so the held-out loss lands ~0.5 (see Known limitations), validation split 0.1, seq len 256.
- Recipe:
needle finetune data.jsonl --epochs 9 --lora-rank 16thenneedle build checkpoints/needle2.pkl --lora adapter.pkl --out tuned.cact.
Training ran on a single Colab T4 and took a few minutes per run.
Known limitations
- False-positive rejection is the weak point. The tuning budget between never calls the tool and calls a tool on everything is narrow for this "is it really addressed to a person?" discrimination. This release is tuned on the extraction side (strong recall on intended targets). If you need stricter rejection, retrain with a higher negative share (25 %+) and stop a little earlier on the validation curve.
- Sequential calls in one process: repeated
complete()calls in the same long-lived process showed degraded output on later calls. For batch work, run one fresh process per text (one query each) or recreate theNeedleobject between batches. - Confidence head is not tuned: tuned weights report
confidenceasNoneand a warning is emitted at construction (expected with needle LoRA blends). - Non-English input: like the base model, out-of-domain text (and Spanish in particular) is not well calibrated.
Files
tuned.cact— merged, exported weights (13.7 MB) forNeedle(weights=...).data.jsonl— the training dataset (browser-search friendly, one JSON object per line).tool.json— theextract_nametool schema.browser-demo.html— self-contained, zero-server browser demo (WASM engine + tuned weights fetched straight from Hugging Face).README.md— this card.
Model tree for Qrzysztof/get-name
Base model
Cactus-Compute/needle2