Semantic persona drift detection for LLM character and agent applications
Project description
🎭 PersIQ
Stop your AI characters from breaking character. Real-time semantic persona drift detection for LLM apps.
The Problem
You build an AI character — a Victorian detective, a pirate, a customer support agent with a specific tone. It works great for the first 10 messages. Then slowly it starts acting like a generic chatbot.
Persona drift is silent, gradual, and almost impossible to catch manually.
Turn 01: "Elementary, my dear Watson." ✅ Perfect
Turn 08: "I deduce the suspect arrived from the east." ✅ Good
Turn 15: "Sure! I'd be happy to help you with that! 😊" ❌ Drifting
Turn 20: "lol yeah totally sounds good to me" 🚨 Severe Drift
The Solution
PersIQ embeds your character definition as an anchor vector, then scores every assistant message for semantic distance from that anchor using a sliding window. When drift is detected, it fires configurable alerts.
Character Definition → Anchor Vector
Each LLM Response → Response Vector
Cosine Distance → Drift Score (0.0 = perfect, 1.0 = max drift)
Sliding Window Mean → Smoothed Score
Threshold Check → Alert / Log / Callback / Exception
Install
pip install persiq
With OpenAI embeddings:
pip install persiq[openai]
Quickstart
Real-Time Tracking
from persiq import PersonaDefinition, SentenceTransformerEmbedder, Tracker
persona = PersonaDefinition(
name="Sherlock Holmes",
description="Cold, analytical, brilliant Victorian detective.",
traits=["logical", "observant", "aloof", "sardonic"],
example_phrases=[
"Elementary, my dear Watson.",
"When you eliminate the impossible, whatever remains must be the truth.",
],
)
embedder = SentenceTransformerEmbedder()
tracker = Tracker(
persona = persona,
embedder = embedder,
threshold = 0.25,
alert_mode = "callback",
callback = lambda a: print(f"⚠️ Drift at turn {a.turn_index}!"),
)
tracker.add_turn("user", "Holmes, what do you deduce?")
tracker.add_turn("assistant", "Elementary. The mud on your boots places you in Kensington.")
state = tracker.state()
print(f"Drift: {state.current_drift:.4f}")
print(f"Drifting: {state.is_drifting}")
Batch Analysis
from persiq import (
PersonaDefinition,
SentenceTransformerEmbedder,
SlidingWindowScorer,
DriftReport,
)
persona = PersonaDefinition.from_json("sherlock.json")
conversation = [
{"role": "user", "content": "What do you observe?"},
{"role": "assistant", "content": "Elementary. The footprints tell us everything."},
{"role": "user", "content": "And your hobbies?"},
{"role": "assistant", "content": "lol I love Netflix and pizza honestly"},
]
embedder = SentenceTransformerEmbedder()
scorer = SlidingWindowScorer(persona, embedder, window_size=5)
scores = scorer.score_conversation(conversation)
report = DriftReport.from_scores(scores, persona, threshold=0.25)
print(report)
report.plot()
CLI
persiq init --output my_persona.json
persiq check \
--persona my_persona.json \
--convo my_chat.json \
--threshold 0.25 \
--plot
Drift Score Reference
| Score | Meaning |
|---|---|
| 0.00 – 0.15 | ✅ Excellent — strongly on-persona |
| 0.15 – 0.25 | 🟡 Moderate — minor deviations |
| 0.25 – 0.35 | 🟠 Concerning — noticeable drift |
| 0.35 – 1.00 | 🔴 Severe — significant character breakdown |
Alert Modes
| Mode | Behavior |
|---|---|
"log" |
Python logger.warning |
"callback" |
Calls your function with DriftAlert object |
"raise" |
Raises DriftAlertException |
"warn" |
Python warnings.warn |
"silent" |
Collects internally, you poll manually |
Embedding Backends
| Backend | Cost | Speed |
|---|---|---|
SentenceTransformerEmbedder |
Free | Fast |
OpenAIEmbedder |
Paid | Faster |
Contributing
git clone https://github.com/himachhatbar17/persIQ
cd persIQ
pip install -e ".[dev]"
pytest tests/ -v
See CONTRIBUTING.md for full guide.
License
MIT © 2024 Hima Chhatbar
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file persiq-0.1.0.tar.gz.
File metadata
- Download URL: persiq-0.1.0.tar.gz
- Upload date:
- Size: 17.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cabbcf69f2d352eb88aed0b1a7fd618ec175810c3d96f9b3c8eea4a3d8427099
|
|
| MD5 |
21d567c3090f73b9cb9e62ba58da08b5
|
|
| BLAKE2b-256 |
a4bb4a2c654ca31c8f5ff1c1965d3e8c96d0a99cb291ed1ed5535220d50f64f1
|
File details
Details for the file persiq-0.1.0-py3-none-any.whl.
File metadata
- Download URL: persiq-0.1.0-py3-none-any.whl
- Upload date:
- Size: 16.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e243e978b1708fd3a49c0bd22c7a658ee31069367ba9d5d5df8338009f34ee65
|
|
| MD5 |
c8d4052b25bc291c419b1ae16ddd351f
|
|
| BLAKE2b-256 |
477ba60139eae344f72d451031c01ed3b93f8545ad25b688d9f7fc75950b71fd
|