CUE is a user-simulator framework that can utilize any LLM to generate messages as realistic users. Based on a user's dialogue history, CUE produces a persona manual: a short list of behavioral commands ("keep requests terse", "ask about price before committing") that can be added to the system prompt of any LLM so it role-plays that user. Underneath the hood, a chat session from user is encoded into a single 1024-d CUE embedding, and a decoder turns that embedding back into text commands. We call this user-conditioned CUE.
You can alternatively use CUE to sample a persona manual representing a novel user for which there is no existing dialogue data available. Learn more in our paper:
CUEing User Simulators: Calibrated User Embeddings for Multi-Turn Benchmarking
This repo contains a production-ready inference runtime to produce text persona manuals using an already-trained CUE model.
Python import name: cue.
Load a published checkpoint from Hugging Face, build a persona manual from
a dialogue or sample one from the prior, and steer any chat model as a
user simulator.
| you want | use |
|---|---|
| weights + remote code | handshake-ai-research/cue |
| example pool | handshake-ai-research/cue-example-pool |
| training annotations | handshake-ai-research/cue-annotations |
| research / training (this repo) | uv run --group research (see training/README.md) |
uv tool install cue-simulator
cue --help
# or
cue-simulator --help
# or as a library (import name is still `cue`)
uv add cue-simulatorBefore the first release, install from a clone with uv sync. Optional extras
are cue-simulator[openai] for OpenAI-compatible endpoints and
cue-simulator[retrieval] for example-pool retrieval.
Cue.from_pretrained prefers the bundled cue_hf adapter when this package is
installed, and otherwise loads Hub remote code with
AutoModel.from_pretrained(..., trust_remote_code=True).
Private Hub weights need HF_TOKEN (or huggingface-cli login).
The repository ships a runnable config under examples/quickstart/:
# 1. Install
uv sync
# 2. Run from the repository root
uv run cue --config examples/quickstart/cue.toml
# 3. Inspect the stable application outputs
cat examples/quickstart/output/steering_prompts.txt
cat examples/quickstart/output/manuals.jsonChange mode = "conditioned" to mode = "sampled" to draw a user from the
diffusion prior instead. Add the three simulator_* fields shown in the example
to also execute one user turn against any configured base simulator.
cue --config is the recommended production interface.
| field | required | default | description |
|---|---|---|---|
mode |
yes | — | conditioned or sampled |
output_dir |
yes | — | Output artifact directory |
model |
no | handshake-ai-research/cue |
Hugging Face repo or local model path |
device |
no | cpu |
Torch device (cpu, cuda, mps) |
conversation_path |
conditioned only | — | JSON, JSONL, or role-prefixed transcript |
session_preprocess |
no | full |
full, strip_document, or user_only |
seed / n |
no | 0 / 1 |
Sampling seed and number of users |
example_pool |
no | — | Separate Hub dataset for style retrieval |
example_retrieval |
no | false |
Retrieve style samples |
simulator_backend |
no | — | openai or hf |
simulator_model |
with backend | — | Any model identifier accepted by that backend |
scenario |
with backend | — | Goal for the role-played user |
history_path |
no | — | Existing simulator conversation |
api_key_env / base_url |
no | — | OpenAI-compatible endpoint configuration |
Paths are resolved from the invocation's working directory, matching the config convention used by other Handshake runtime packages.
from cue import Cue
cue = Cue.from_pretrained("handshake-ai-research/cue", device="cuda") # or "cpu" / "mps"
manual = cue.from_conversation([
{"role": "user", "content": "can you tighten this paragraph"},
{"role": "assistant", "content": "Sure — here is a shorter version."},
{"role": "user", "content": "still too long, cut it in half"},
])
print(manual.steering_prompt) # paste this text snippet into a user-simulator system prompt used with any LLM
print(manual.commands) # flat command listAccepted conversation formats: OpenAI-style message lists, role-prefixed transcripts
(user: … / assistant: …), or files via cue.conversation.load_conversation.
manual = cue.sample(seed=0) # one user
manuals = cue.sample(n=4, seed=0) # batch → list[PersonaManual]Requires sampler.pt on the Hub repo (shipped with handshake-ai-research/cue).
from cue import OpenAICompatSimulator, HuggingFaceSimulator
from cue.simulator import run_user_turn
# OpenAI, vLLM, OpenRouter, Azure, … — anything Chat Completions–compatible
sim = OpenAICompatSimulator("gpt-4o-mini")
# or: sim = OpenAICompatSimulator("meta-llama/…", base_url="http://localhost:8000/v1", api_key="EMPTY")
# or: sim = HuggingFaceSimulator("meta-llama/Llama-3.1-8B-Instruct")
user_msg = run_user_turn(
sim,
scenario="You want help planning a 3-day trip to Lisbon on a tight budget.",
history=[], # prior turns in assistant POV (user/assistant roles)
manual=manual, # PersonaManual or a raw steering string
)
print(user_msg)run_user_turn flips roles so the simulator sees its own past messages as its own.
The config interface is stable. The subcommands below are convenient direct interfaces for scripts and debugging:
# Conversation → steering prompt
cue conditioned -c conversation.json
cue conditioned -t 'user: hi\nassistant: hello\nuser: more detail please'
# Sample synthetic users
cue sample -n 3 --seed 0
# One steered user turn against a base LM
export OPENAI_API_KEY=...
cue chat --scenario 'Book a cheap flight to Boston' -c conversation.json \
--simulator openai --sim-model gpt-4o-mini
cue chat --scenario '…' --sample --simulator hf \
--sim-model meta-llama/Llama-3.1-8B-InstructConfig-driven runs write:
manuals.json: structured general, user-specific, and style commands.steering_prompts.txt: prompts ready to inject into a simulator.simulated_users.json: generated user turns when a simulator is configured.info.json: resolved non-secret config and output paths.
PersonaManual fields:
| field | meaning |
|---|---|
steering_prompt |
Ready-to-inject text for a user-simulator system message |
commands |
Flat list of behavioral commands |
general / specific / style |
Per-head commands when dual decode is on |
examples |
Style samples (decoded or retrieved) |
embedding |
Raw CUE vector (list[float], optional) |
source |
"conditioned" or "sampled" |
Default is full (keep the whole dialogue). For document-heavy assistants (e.g. long
drafts), pass session_preprocess="strip_document" so the encoder is not dominated by
the document body. user_only keeps user turns only.
The cue_hf package lives in huggingface/ and ships inside the
cue-simulator wheel (import cue_hf, CLI cue-hf). Hub checkpoints still
flatten this package into remote code; after changing adapter sources, regenerate
that bundle as described in huggingface/VENDORED.md.
uv sync --group dev
PYTHONPATH=huggingface uv run --group dev pytest huggingface/testsTraining, annotation, rollouts, and the eight paper metrics are an opt-in checkout path, not part of the published wheel:
uv sync # inference runtime only (Python 3.12–3.14)
uv sync --group dev # plus pytest/ruff/mypy
uv run --group research cue-train --help
uv run --group fma cue-fma --help # Tau2 failure-mode analysisAlways pass --group research on later uv run commands; a plain uv run
re-syncs the default groups and can remove the research package. On Python 3.14
use --python 3.13. FMA artifacts default to the ignored
training/artifacts/fma/ directory and are not stored in this repo. Details:
training/README.md.
See DEVELOPMENT.md for Hatch commands, testing, type
checking, formatting, packaging, and CI. Model-download tests are marked
model and excluded from the default unit test run.
@article{kantharuban2026cue,
title={CUEing User Simulators: Calibrated User Embeddings for Multi-Turn Benchmarking},
author={Kantharuban, Anjali and Mueller, Jonas},
journal={arXiv preprint arXiv:2610.02460},
year={2026}
}This software is licensed under Apache-2.0.
Model weights on Hugging Face carry their own card/license.