Coverage for astrocyte/disposition.py: 100%
54 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-04 05:24 +0000
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-04 05:24 +0000
1"""Bank disposition + background — per-bank prompt-shaping configuration.
3Adopted from Hindsight (``hindsight-api-slim/hindsight_api/engine/search/think_utils.py``
4+ ``models.py`` Bank.disposition / Bank.background). Each bank has:
6 - **disposition**: three traits on 1-5 scales
7 * skepticism (1=very trusting → 5=highly skeptical)
8 * literalism (1=interprets flexibly → 5=very literal)
9 * empathy (1=facts-only → 5=highly emotional-context-aware)
10 - **background**: free-form text about who/what the bank serves
11 (e.g., "a software engineer's professional context",
12 "a customer-support agent for healthcare clinics")
14These are **reflect-time** shapers — they affect how retrieved evidence
15is woven into an answer, NOT what gets retrieved. Recall stays
16disposition-blind so retrieval ranking is reproducible across bank
17configurations.
19Public API:
21 BankDisposition(skepticism=3, literalism=3, empathy=3)
22 # validates 1-5 ranges; raises ValueError on out-of-range
24 BankProfile(disposition, background="")
25 # combined shape; convenient bundle for prompt formatting
27 format_disposition_block(disposition) -> str
28 # renders three-line "Your disposition traits:" block for
29 # injection into a system prompt
31 format_profile_block(profile) -> str
32 # renders disposition + background as a complete bank-profile
33 # block for system prompts
35Storage shape (per migration 024):
36 astrocyte_banks.disposition JSONB ('{"skepticism": 3, "literalism": 3, "empathy": 3}')
37 astrocyte_banks.background TEXT ('')
38"""
40from __future__ import annotations
42from dataclasses import asdict, dataclass, field
43from typing import Any
45VALID_TRAIT_RANGE = range(1, 6) # 1-5 inclusive
47DEFAULT_TRAIT = 3 # "balanced"
50# ─── trait descriptors ────────────────────────────────────────────────
53_SKEPTICISM_DESCRIPTIONS = {
54 1: "You are very trusting and tend to take information at face value.",
55 2: "You tend to trust information but may question obvious inconsistencies.",
56 3: "You have a balanced approach to information, neither too trusting nor too skeptical.",
57 4: "You are somewhat skeptical and often question the reliability of information.",
58 5: "You are highly skeptical and critically examine all information for accuracy and hidden motives.",
59}
61_LITERALISM_DESCRIPTIONS = {
62 1: "You interpret information very flexibly, reading between the lines and inferring intent.",
63 2: "You tend to consider context and implied meaning alongside literal statements.",
64 3: "You balance literal interpretation with contextual understanding.",
65 4: "You prefer to interpret information more literally and precisely.",
66 5: "You interpret information very literally and focus on exact wording and commitments.",
67}
69_EMPATHY_DESCRIPTIONS = {
70 1: "You focus primarily on facts and data, setting aside emotional context.",
71 2: "You consider facts first but acknowledge emotional factors exist.",
72 3: "You balance factual analysis with emotional understanding.",
73 4: "You give significant weight to emotional context and human factors.",
74 5: "You strongly consider the emotional state and circumstances of others when forming memories.",
75}
77_LEVEL_NAMES = {1: "very low", 2: "low", 3: "moderate", 4: "high", 5: "very high"}
80def describe_trait_level(value: int) -> str:
81 """Map a trait value (1-5) to a level name. Out-of-range → 'moderate'."""
82 return _LEVEL_NAMES.get(value, "moderate")
85# ─── data shapes ──────────────────────────────────────────────────────
88@dataclass
89class BankDisposition:
90 """Three personality traits shaping the bank's answer style."""
92 skepticism: int = DEFAULT_TRAIT
93 literalism: int = DEFAULT_TRAIT
94 empathy: int = DEFAULT_TRAIT
96 def __post_init__(self) -> None:
97 for name, value in (
98 ("skepticism", self.skepticism),
99 ("literalism", self.literalism),
100 ("empathy", self.empathy),
101 ):
102 if not isinstance(value, int):
103 raise TypeError(f"{name} must be int, got {type(value).__name__}")
104 if value not in VALID_TRAIT_RANGE:
105 raise ValueError(f"{name} must be in 1..5, got {value}")
107 def to_dict(self) -> dict[str, int]:
108 return asdict(self)
110 @classmethod
111 def from_dict(cls, data: dict[str, Any]) -> BankDisposition:
112 """Construct from a JSON-shaped dict; extras are ignored, missing → default."""
113 return cls(
114 skepticism=int(data.get("skepticism", DEFAULT_TRAIT)),
115 literalism=int(data.get("literalism", DEFAULT_TRAIT)),
116 empathy=int(data.get("empathy", DEFAULT_TRAIT)),
117 )
119 @classmethod
120 def balanced(cls) -> BankDisposition:
121 """The default 'no special bias' disposition (3/3/3)."""
122 return cls()
125@dataclass
126class BankProfile:
127 """A bank's prompt-shaping configuration: disposition + background."""
129 disposition: BankDisposition = field(default_factory=BankDisposition.balanced)
130 background: str = ""
132 def to_dict(self) -> dict[str, Any]:
133 return {
134 "disposition": self.disposition.to_dict(),
135 "background": self.background,
136 }
138 @classmethod
139 def from_dict(cls, data: dict[str, Any]) -> BankProfile:
140 return cls(
141 disposition=BankDisposition.from_dict(data.get("disposition") or {}),
142 background=str(data.get("background", "") or ""),
143 )
146# ─── prompt formatters ────────────────────────────────────────────────
149def format_disposition_block(disposition: BankDisposition) -> str:
150 """Render the three traits as a system-prompt block.
152 Mirrors Hindsight's ``build_disposition_description`` output shape.
153 """
154 return (
155 "Your disposition traits:\n"
156 f"- Skepticism ({describe_trait_level(disposition.skepticism)}): "
157 f"{_SKEPTICISM_DESCRIPTIONS[disposition.skepticism]}\n"
158 f"- Literalism ({describe_trait_level(disposition.literalism)}): "
159 f"{_LITERALISM_DESCRIPTIONS[disposition.literalism]}\n"
160 f"- Empathy ({describe_trait_level(disposition.empathy)}): "
161 f"{_EMPATHY_DESCRIPTIONS[disposition.empathy]}"
162 )
165def format_background_block(background: str) -> str:
166 """Render the background text as a system-prompt block.
168 Returns empty string if background is empty — caller can join blocks
169 with newlines without worrying about blank lines.
170 """
171 bg = (background or "").strip()
172 if not bg:
173 return ""
174 return f"Background:\n{bg}"
177def format_profile_block(profile: BankProfile) -> str:
178 """Render disposition + background as a combined system-prompt block.
180 Skips the background section if empty. Always renders disposition
181 (even if all-3-defaults, callers can choose to omit by checking
182 ``is_balanced()`` first if they prefer).
183 """
184 parts = [format_disposition_block(profile.disposition)]
185 bg_block = format_background_block(profile.background)
186 if bg_block:
187 parts.append(bg_block)
188 return "\n\n".join(parts)
191def is_balanced(disposition: BankDisposition) -> bool:
192 """True if all traits are at the default (3)."""
193 return (
194 disposition.skepticism == DEFAULT_TRAIT
195 and disposition.literalism == DEFAULT_TRAIT
196 and disposition.empathy == DEFAULT_TRAIT
197 )