persona_preferences
Persona preference memory system.
Persistent storage for the persona’s opinions, preferences, and ideals. This is not user memory — it is identity coherence for an entity that ceases to exist between inference calls.
Redis key pattern: stargazer:persona_pref:{persona_id}:{preference_id}
Each preference is a Redis HASH with full provenance including the exact triggering user and message IDs, plus optional links to KG entities and related user IDs for hard-filter retrieval.
- class persona_preferences.PersonaPreferenceManager(redis, config=None)[source]
Bases:
objectManager for persistent persona preference memory.
Backed by Redis hashes with a RediSearch HNSW vector index for semantic retrieval and optional hard-filter queries by related user IDs.
- Parameters:
- __init__(redis, config=None)[source]
Initialize the manager with a Redis client and optional config.
Stores the async Redis handle used by every CRUD/search method and resolves the default base persona identifier from config (falling back to
"stargazer"when absent). No I/O is performed here — the RediSearch index is created lazily viaensure_index().The constructed manager is shared on the tool context as
ctx.persona_pref_manager(declared intool_context.pyand defaulted toNoneininference_main.py), letting the persona preference tools reach it without a circular import. The resolved_base_persona_idis later read by the tool helpers intools/persona_preferences.py(e.g._resolve_persona_idandinspect_persona_preferences) to default the persona. No directPersonaPreferenceManager(...)construction site was found in the repository, so the instance is wired up dynamically by the inference worker’s setup path.- Parameters:
- Return type:
None
- async ensure_index()[source]
Create the RediSearch HNSW vector index if it does not exist.
Idempotently provisions the
idx:persona_prefsindex over allstargazer:persona_pref:HASH keys so that the semantic-retrieval and hard-filter queries used bysearch_preferences()andget_preferences_for_injection()can run. First probes viaFT.INFOand returns early if the index already exists; otherwise issuesFT.CREATEdefining TAG fields (persona_id,category,active,related_user_ids_index), a NUMERICstrengthfield, and a 3072-dimFLOAT32HNSWembeddingvector with cosine distance. All failures are logged rather than raised, so a transient Redis hiccup never crashes the caller. No external caller for the persona-preference manager was found in the repo; it is expected to be invoked during inference-worker setup or lazily before first search.- Return type:
- async add_preference(persona_id, name, opinion, category, *, triggering_user_id, triggering_message_id, source_platform, source_channel_id, strength=0.5, related_user_ids=None, related_kg_uuids=None)[source]
Embed an opinion and persist it as a new preference hash.
Creates a fresh preference: it mints an id via
_new_id(), stamps provenance timestamps via_now_iso(), embeds the opinion text via_embed(), serializes that vector with_vec_to_bytes(), andHSETsthe full record (name, opinion, validated category, strength, triggering user/message/platform/channel, related user ids and KG uuids, evolution history, and the embedding) under_pref_key(). Unknown categories are coerced to"philosophical";nameandopinionare truncated to 256 and 4096 chars. Touches Redis (oneHSET) and the Gemini embedding pool. Called bypersona_preference_extraction.py(around line 517) when an opinion is detected, and by themanage_persona_preferencestool intools/persona_preferences.py(around lines 167 and 191).- Parameters:
persona_id (
str) – Persona that owns this preference.name (
str) – Short human-readable label (truncated to 256 chars).opinion (
str) – The opinion text that is embedded and stored (truncated to 4096 chars).category (
str) – One ofVALID_CATEGORIES; otherwise stored as"philosophical".triggering_user_id (
str) – User id that prompted this preference.triggering_message_id (
str) – Message id that prompted it.source_platform (
str) – Originating platform (e.g.discord).source_channel_id (
str) – Originating channel id.strength (
float) – Initial conviction in[0.0, 1.0](default0.5).related_user_ids (
list[str] |None) – User ids this opinion is about, stored both as JSON and as a comma-joined TAG index for hard-filter retrieval.related_kg_uuids (
list[str] |None) – Linked knowledge-graph entity uuids.
- Returns:
The stored preference fields (the raw
embeddingbytes excluded) including the newly mintedid.- Return type:
- async reinforce_preference(preference_id, persona_id)[source]
Reinforce an existing preference, bumping its strength.
Loads the preference HASH, raises its strength along the diminishing- returns curve via
_reinforce_strength(), incrementsreinforcement_count, and refresheslast_reinforcedvia_now_iso(), then writes those three fields back. Reads and writes Redis (oneHGETALLplus oneHSET) at the key from_pref_key(); returns anerrordict (rather than raising) when the preference is missing. Called bypersona_preference_extraction.py(around line 472) when an opinion matches an existing one, and by themanage_persona_preferencestool intools/persona_preferences.py(around line 154).
- async evolve_preference(preference_id, persona_id, new_opinion, reason)[source]
Rewrite a preference’s opinion and log the change in its history.
Models a genuine change of mind: it appends an entry (old opinion, new opinion, timestamp via
_now_iso(), and reason) to the preference’sevolution_history, re-embeds the new opinion via_embed(), serializes that vector via_vec_to_bytes(), and writes back the updated opinion text (truncated to 4096 chars), refreshedlast_reinforced, history, and embedding. Strength is reset to0.6to reflect that an evolved belief is held with renewed but not maximal conviction. Reads and writes Redis (oneHGETALLplus oneHSET) at the key from_pref_key()and calls the Gemini embedding pool; returns anerrordict when the preference is missing. Called by themanage_persona_preferencestool intools/persona_preferences.py(around line 318).- Parameters:
- Returns:
Updated
id,name,new_opinion,strength(0.6), and the newevolutionscount; an{"error": ...}dict if the preference was not found.- Return type:
- async retract_preference(preference_id, persona_id, reason='')[source]
Soft-delete a preference by marking it inactive.
Retracts a preference without destroying it: it sets
activeto"0"so the@active:{1}filter excludes it from searches and injection, while leaving the full record (and, when areasonis given, a"retraction"entry appended toevolution_historywith a_now_iso()timestamp) intact for provenance. Reads and writes Redis (oneHGETALLplus oneHSET) at the key from_pref_key(); returns anerrordict when the preference is missing. Called by themanage_persona_preferencestool intools/persona_preferences.py(around line 358).- Parameters:
- Returns:
id,name, andactive(False) on success; an{"error": ...}dict if the preference was not found.- Return type:
- async get_preference(preference_id, persona_id)[source]
Fetch and decode a single preference by id.
Loads one preference HASH at the key from
_pref_key()and returns it as a plain dict via_decode_pref()(which drops the raw embedding bytes and coercesactiveto a bool), orNonewhen no such key exists. Performs a single RedisHGETALL. No caller of this manager method was found in the repository; it is a convenience accessor available for tool or diagnostic use.
- async list_preferences(persona_id, category=None, include_retracted=False, limit=50)[source]
List a persona’s preferences, strongest first.
Enumerates every preference HASH for a persona by
KEYS-scanning thestargazer:persona_pref:{persona_id}:*pattern, decoding each via_decode_pref(), then filtering out retracted entries (unlessinclude_retracted) and non-matching categories before sorting by descendingstrengthand capping atlimit. Reads Redis (oneKEYSplus oneHGETALLper key) and does not use the vector index. Called bystats()(withinclude_retracted=True) and by theinspect_persona_preferencestool intools/persona_preferences.py(around lines 240, 399, 408, and 416).- Parameters:
- Returns:
Decoded preferences sorted by descending strength.
- Return type:
- async stats(persona_id)[source]
Summarize a persona’s preference memory.
Pulls every preference (including retracted ones) via
list_preferences()with a large limit, then tallies active versus retracted totals and builds a per-category histogram over the active set. Its only I/O is the underlyinglist_preferencesRedis scan. Called by theinspect_persona_preferencestool intools/persona_preferences.py(around line 396) to render thestatsinspection action.
- async search_preferences(persona_id, query_embedding, top_k=10, include_retracted=False)[source]
Run a KNN vector search over a persona’s preferences.
Performs the core semantic retrieval: it serializes the query vector via
_vec_to_bytes()and issues anFT.SEARCHKNN query against theidx:persona_prefsHNSW index, constrained by a@persona_idTAG filter and (unlessinclude_retracted) an@active:{1}filter, returning the toptop_kmatches sorted by ascending cosine distance (score). Raw results are normalized via_parse_search_results(); any search failure is logged and returns an empty list rather than raising. Reads Redis via the search index. Called byfind_conflicts()andget_preferences_for_injection()internally, and by theinspect_persona_preferencestool intools/persona_preferences.py(around line 231).- Parameters:
- Returns:
Matching preferences, each carrying a
score(cosine distance), or an empty list on error.- Return type:
- async find_conflicts(persona_id, new_opinion_embedding, threshold=0.8)[source]
Find active preferences semantically close to a new opinion.
Supports the dedup/conflict decision when a candidate opinion is detected: it runs a top-5
search_preferences()KNN query, converts each result’s cosine distance into a similarity (1.0 - score), and keeps only matches at or abovethreshold, annotating each kept match with itssimilarity. The caller uses these to decide whether to reinforce an existing preference, flag a conflict, or store a brand-new one. Its only I/O is the underlying search-index query. Called bypersona_preference_extraction.py(around line 457) and by themanage_persona_preferencestool intools/persona_preferences.py(around line 136).- Parameters:
- Returns:
Matching preferences, each augmented with a
similarityfield; empty if none clear the threshold.- Return type:
- async get_preferences_for_injection(persona_id, query_embedding, max_count=8, max_chars=2000, active_user_ids=None)[source]
Select the persona preferences to inject into the system prompt.
Performs the dual-pass retrieval that surfaces the persona’s relevant opinions for a given conversational moment. Pass one runs a
search_preferences()KNN query (top_kofmax_count * 2) for topic-relevant preferences; pass two issues per-userFT.SEARCHhard-filter queries on the@related_user_ids_indexTAG (with:and-escaped) to pull in opinions specifically about the people currently in the conversation. The two result sets are merged, deduplicated by id, sorted by the blended relevance-times-strength score from the local_sort_key()closure, then truncated tomax_countitems andmax_charsof opinion text and shaped via_format_for_injection(). Reads Redis via the search index; per-user query failures are logged and skipped. Called byprompt_context.py(around line 3190) while assembling the runtime prompt context.- Parameters:
persona_id (
str) – Persona whose preferences to retrieve.query_embedding (
list[float]) – Embedding of the current conversational context for the semantic pass.max_count (
int) – Maximum number of preferences to inject (default 8).max_chars (
int) – Maximum total opinion characters to inject (default 2000).active_user_ids (
list[str] |None) – User ids present in the conversation, used for the hard-filter pass.
- Returns:
Injection-shaped preference dicts (from
_format_for_injection()), ranked and capped.- Return type: