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: object

Manager 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:
  • redis (Any) – Async redis.asyncio.Redis client (already connected).

  • config (Config | None) – Optional bot Config for tuning knobs.

__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 via ensure_index().

The constructed manager is shared on the tool context as ctx.persona_pref_manager (declared in tool_context.py and defaulted to None in inference_main.py), letting the persona preference tools reach it without a circular import. The resolved _base_persona_id is later read by the tool helpers in tools/persona_preferences.py (e.g. _resolve_persona_id and inspect_persona_preferences) to default the persona. No direct PersonaPreferenceManager(...) construction site was found in the repository, so the instance is wired up dynamically by the inference worker’s setup path.

Parameters:
  • redis (Any) – A connected async redis.asyncio.Redis client used for all hash and FT.* index operations.

  • config (Config | None) – Optional bot Config providing tuning knobs; only persona_pref_base_persona_id is read here. When None, the base persona id defaults to "stargazer".

Return type:

None

async ensure_index()[source]

Create the RediSearch HNSW vector index if it does not exist.

Idempotently provisions the idx:persona_prefs index over all stargazer:persona_pref: HASH keys so that the semantic-retrieval and hard-filter queries used by search_preferences() and get_preferences_for_injection() can run. First probes via FT.INFO and returns early if the index already exists; otherwise issues FT.CREATE defining TAG fields (persona_id, category, active, related_user_ids_index), a NUMERIC strength field, and a 3072-dim FLOAT32 HNSW embedding vector 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:

None

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(), and HSETs the 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"; name and opinion are truncated to 256 and 4096 chars. Touches Redis (one HSET) and the Gemini embedding pool. Called by persona_preference_extraction.py (around line 517) when an opinion is detected, and by the manage_persona_preferences tool in tools/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 of VALID_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] (default 0.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 embedding bytes excluded) including the newly minted id.

Return type:

dict[str, Any]

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(), increments reinforcement_count, and refreshes last_reinforced via _now_iso(), then writes those three fields back. Reads and writes Redis (one HGETALL plus one HSET) at the key from _pref_key(); returns an error dict (rather than raising) when the preference is missing. Called by persona_preference_extraction.py (around line 472) when an opinion matches an existing one, and by the manage_persona_preferences tool in tools/persona_preferences.py (around line 154).

Parameters:
  • preference_id (str) – Id of the preference to reinforce.

  • persona_id (str) – Persona that owns it.

Returns:

Updated id, name, strength, and reinforcement_count on success; an {"error": ...} dict if the preference was not found.

Return type:

dict[str, Any]

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’s evolution_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), refreshed last_reinforced, history, and embedding. Strength is reset to 0.6 to reflect that an evolved belief is held with renewed but not maximal conviction. Reads and writes Redis (one HGETALL plus one HSET) at the key from _pref_key() and calls the Gemini embedding pool; returns an error dict when the preference is missing. Called by the manage_persona_preferences tool in tools/persona_preferences.py (around line 318).

Parameters:
  • preference_id (str) – Id of the preference to evolve.

  • persona_id (str) – Persona that owns it.

  • new_opinion (str) – Replacement opinion text (truncated to 4096 chars and re-embedded).

  • reason (str) – Why the opinion changed, recorded in the history log.

Returns:

Updated id, name, new_opinion, strength (0.6), and the new evolutions count; an {"error": ...} dict if the preference was not found.

Return type:

dict[str, Any]

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 active to "0" so the @active:{1} filter excludes it from searches and injection, while leaving the full record (and, when a reason is given, a "retraction" entry appended to evolution_history with a _now_iso() timestamp) intact for provenance. Reads and writes Redis (one HGETALL plus one HSET) at the key from _pref_key(); returns an error dict when the preference is missing. Called by the manage_persona_preferences tool in tools/persona_preferences.py (around line 358).

Parameters:
  • preference_id (str) – Id of the preference to retract.

  • persona_id (str) – Persona that owns it.

  • reason (str) – Optional explanation; when non-empty it is recorded in the evolution history.

Returns:

id, name, and active (False) on success; an {"error": ...} dict if the preference was not found.

Return type:

dict[str, Any]

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 coerces active to a bool), or None when no such key exists. Performs a single Redis HGETALL. No caller of this manager method was found in the repository; it is a convenience accessor available for tool or diagnostic use.

Parameters:
  • preference_id (str) – Id of the preference to fetch.

  • persona_id (str) – Persona that owns it.

Returns:

The decoded preference, or None if not found.

Return type:

dict[str, Any] | None

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 the stargazer:persona_pref:{persona_id}:* pattern, decoding each via _decode_pref(), then filtering out retracted entries (unless include_retracted) and non-matching categories before sorting by descending strength and capping at limit. Reads Redis (one KEYS plus one HGETALL per key) and does not use the vector index. Called by stats() (with include_retracted=True) and by the inspect_persona_preferences tool in tools/persona_preferences.py (around lines 240, 399, 408, and 416).

Parameters:
  • persona_id (str) – Persona whose preferences to list.

  • category (str | None) – If set, only preferences in this category are returned.

  • include_retracted (bool) – When True, also include inactive (retracted) preferences.

  • limit (int) – Maximum number of preferences to return (default 50).

Returns:

Decoded preferences sorted by descending strength.

Return type:

list[dict[str, Any]]

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 underlying list_preferences Redis scan. Called by the inspect_persona_preferences tool in tools/persona_preferences.py (around line 396) to render the stats inspection action.

Parameters:

persona_id (str) – Persona to summarize.

Returns:

persona_id plus total, active, retracted counts and a by_category mapping of active preferences per category.

Return type:

dict[str, Any]

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 an FT.SEARCH KNN query against the idx:persona_prefs HNSW index, constrained by a @persona_id TAG filter and (unless include_retracted) an @active:{1} filter, returning the top top_k matches 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 by find_conflicts() and get_preferences_for_injection() internally, and by the inspect_persona_preferences tool in tools/persona_preferences.py (around line 231).

Parameters:
  • persona_id (str) – Persona whose preferences to search.

  • query_embedding (list[float]) – The query vector (EMBED_DIM long).

  • top_k (int) – Number of nearest neighbors to return (default 10).

  • include_retracted (bool) – When True, also search inactive preferences.

Returns:

Matching preferences, each carrying a score (cosine distance), or an empty list on error.

Return type:

list[dict[str, Any]]

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 above threshold, annotating each kept match with its similarity. 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 by persona_preference_extraction.py (around line 457) and by the manage_persona_preferences tool in tools/persona_preferences.py (around line 136).

Parameters:
  • persona_id (str) – Persona whose preferences to compare against.

  • new_opinion_embedding (list[float]) – Embedding of the candidate opinion.

  • threshold (float) – Minimum similarity to count as a match (default _CONFLICT_THRESHOLD, 0.80).

Returns:

Matching preferences, each augmented with a similarity field; empty if none clear the threshold.

Return type:

list[dict[str, Any]]

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_k of max_count * 2) for topic-relevant preferences; pass two issues per-user FT.SEARCH hard-filter queries on the @related_user_ids_index TAG (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 to max_count items and max_chars of opinion text and shaped via _format_for_injection(). Reads Redis via the search index; per-user query failures are logged and skipped. Called by prompt_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:

list[dict[str, Any]]