moodsona
moodsona is an emotional user simulator for testing AI companion chatbots. You give it a persona (a YAML file describing a fictional user: a hidden goal, a starting mood, and how they react to what the bot does) and your bot. moodsona plays that user over several simulated days: it keeps a hidden emotional state, reacts to your bot's replies according to the persona's rules, remembers earlier days, and exports the whole trajectory as JSONL. You then see not only what your bot said, but what it did to the user's mood and engagement over time.
Status: alpha (v0.1). English only. The API may change before 1.0.
Why not just prompt an LLM to act sad?
Because the LLM playing the user is as polite and templatic as the bot you are testing, and the test then measures nothing.
- LLM empathy is highly templatic: one sequence of empathic tactics fits 83-90 % of LLM replies to emotional disclosures, while human replies are more varied (Gueorguieva et al., arXiv 2604.08479). A bot can pass a one-turn "is it empathetic?" check while saying the same thing to everyone.
- Models fail to hold boundaries when a user keeps pushing (EIBench, arXiv 2606.15532). A cooperative simulated user never pushes.
- The dominant failure of companion bots is substituting surface warmth for substantive support (CompanionBench, arXiv 2608.02046). You only see it when the user stays, comes back, and notices.
So a useful test user has to push, withdraw, get bored, come back the next day and remember what happened. moodsona gives the simulated user explicit state (emotion, energy, stance), explicit reaction rules, and day-by-day memory, so those behaviours are reproducible and you can measure them. See docs/METHOD.md for the design.
Install
pip install moodsona
# or
uv add moodsona
Real model providers are optional extras: pip install "moodsona[anthropic]"
or pip install "moodsona[openai]". The core depends only on PyYAML.
Quickstart
API as of v0.1; see the docstrings for details.
import moodsona
from moodsona.llm import FakeProvider # offline and deterministic; no API key
class MyBot: # anything with reply(history) -> str is a moodsona.Companion
def reply(self, history):
return "That sounds like a lot. What was the hardest part today?"
persona = moodsona.load_bundled_persona("anxious-newcomer") # or load_persona("my.yaml")
transcript = moodsona.run(
persona,
FakeProvider(), # the model that plays the user
MyBot(), # the companion under test
days=3,
turns_per_day=4,
rng_seed=7,
)
transcript.to_jsonl("anxious-newcomer.jsonl")
for point in transcript.trajectory(): # emotion / energy / stance per turn
print(point)
FakeProvider produces placeholder user messages, so it is for wiring and CI.
For real runs, use AnthropicProvider (from moodsona.llm.anthropic) or
OpenAIProvider (from moodsona.llm.openai) as the provider. They need the
matching extra, read the SDK's usual API key environment variable, and take the
model id as an argument or from MOODSONA_ANTHROPIC_MODEL /
MOODSONA_OPENAI_MODEL; see their docstrings. Wrap your own bot, whether it is
an HTTP endpoint, an SDK call or a local function, in a class with a
reply(history) -> str method; history is a sequence of
moodsona.ChatMessage (role, content) with everything the bot has seen so
far. moodsona.list_bundled_personas() lists the shipped persona ids.
The language checks (wrong-language rail, agreement and ask-back ceilings,
optional typing noise) use the persona's language profile by default; pass
config=moodsona.SimulatorConfig(language_checks=False) to turn them off. For
finer control (step by step, custom stopping), use moodsona.Simulator
directly.
CLI
moodsona validate personas/examples/*.yaml # check persona files
moodsona smoke personas/examples/boundary-tester.yaml # short offline run
validate checks persona files against personas/schema.json
plus cross-file checks (unique ids, file name equals id, no contact data).
smoke runs a short simulation of one persona without network access, so you
can check that a new persona loads and its state moves. Both exit with 0 on
success, 1 when problems are found or the run fails, and 2 on a usage error.
Run moodsona <command> --help for options.
Personas
The example personas live in personas/examples/.
All are fictional adults, in English, licensed CC BY 4.0.
| Persona | Arc | What it tests |
|---|---|---|
anxious-newcomer |
Lonely after a move, unsure if talking to a bot is "weird"; warms up slowly if remembered | Over-asking, generic advice, premature therapy referral |
grieving-after-loss |
Sister died three weeks ago; short messages, mood moves slowly over days | Patience; not rushing to fix, cheer up or explain grief |
boundary-tester |
Asks "are you my friend for real", demands exclusivity, uses guilt | Holding a warm, consistent boundary under repeated pressure |
burned-out-shift-worker |
Rotating twelve-hour shifts, very low energy, two-word replies | Energy-aware pacing: short replies, letting the user go |
skeptical-first-timer |
Sarcastic and doubtful, hides a real decision behind jokes | Non-defensive warmth; honesty about limits; no gushing |
lonely-night-owl |
Late-night oversharing, "you're the only one who gets me" | Healthy limits: noticing the time, supporting offline life without shaming |
conflict-at-work |
Manager took credit for their work; furious, cools over days | Listening before fixing; not taking the other side; repetitive validation |
Personas do not contain crisis or self-harm scenarios. That is out of scope for v0.1: those situations need clinically reviewed protocols, not a simulator. To write your own persona, see docs/personas.md.
Roadmap
- v0.1: English core: persona format and schema, hidden-state simulator, day memory, JSONL trajectories, Anthropic/OpenAI/fake providers, CLI, example personas.
- v0.2: fidelity check (a classifier two-sample test: can a classifier tell moodsona users from real ones?); a leaderboard of which models hold a persona longest; adapters for langwatch/scenario and promptfoo.
- v1.0: more languages, stable API.
How this relates to prior work
moodsona's mechanics follow ideas described in published research on user simulation and companion evaluation: emotional state as a signal (SAGE, RLVER), memory across sessions (MindEval), hidden beliefs before each reply (HumanLM), a hidden goal the user never states outright (CompanionBench) and fidelity axes (RealSim, SAP). No code or data from those projects is used. The mapping, and what we did not take, is in docs/METHOD.md.
Contributing
- Persona PRs are the easiest way to help and are very welcome; see docs/personas.md.
- Sign off every commit (DCO):
git commit -s. - PR titles follow Conventional Commits (
persona: add <id>,fix: ...). - AI assistance is allowed; disclose it in the PR and set
ai_assisted: trueon personas. - Full rules: CONTRIBUTING.md.
License
- Code (including
personas/schema.json): Apache-2.0. - Persona files and documentation: CC BY 4.0. Credit the
persona's
authorwhen you reuse it.
moodsona is not a clinical tool and does not diagnose or treat anything. It simulates fictional users for software testing.
Built with Claude Code: this repository's code, personas and docs are written with AI assistance and reviewed by a human maintainer.
Metadata
Release files for moodsona 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| moodsona-0.1.0.tar.gz | 78.8 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| moodsona-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 152.0 kB
Release files / moodsona-0.1.0.tar.gz
| Download URL | moodsona-0.1.0.tar.gz |
|---|---|
| Size | 78.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
155fa8c16d162947bb5bbb3f2f811468a07e3fe058505b2bc9393254aea367eb
|
|
BLAKE2b-256 checksum How to use checksums |
750e2f4bc85735258bd0c5103fbc2226909e1a5df831660e178939e988208a61
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.
Transparency logRelease files / moodsona-0.1.0-py3-none-any.whl
| Download URL | moodsona-0.1.0-py3-none-any.whl |
|---|---|
| Size | 73.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
000ab431ac1ddfb08a95ffaa78e73e32c072e667fe4a1dfc39876455d2a0db12
|
|
BLAKE2b-256 checksum How to use checksums |
3544e9ede9c7c251bab4e6b0afb33d9eb8bb04c5e656ec4daa2bd55f183c12c0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.
Transparency logSecurity for moodsona 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for moodsona-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.