Coverage for astrocyte/disposition.py: 100%

54 statements  

« 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. 

2 

3Adopted from Hindsight (``hindsight-api-slim/hindsight_api/engine/search/think_utils.py`` 

4+ ``models.py`` Bank.disposition / Bank.background). Each bank has: 

5 

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") 

13 

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. 

18 

19Public API: 

20 

21 BankDisposition(skepticism=3, literalism=3, empathy=3) 

22 # validates 1-5 ranges; raises ValueError on out-of-range 

23 

24 BankProfile(disposition, background="") 

25 # combined shape; convenient bundle for prompt formatting 

26 

27 format_disposition_block(disposition) -> str 

28 # renders three-line "Your disposition traits:" block for 

29 # injection into a system prompt 

30 

31 format_profile_block(profile) -> str 

32 # renders disposition + background as a complete bank-profile 

33 # block for system prompts 

34 

35Storage shape (per migration 024): 

36 astrocyte_banks.disposition JSONB ('{"skepticism": 3, "literalism": 3, "empathy": 3}') 

37 astrocyte_banks.background TEXT ('') 

38""" 

39 

40from __future__ import annotations 

41 

42from dataclasses import asdict, dataclass, field 

43from typing import Any 

44 

45VALID_TRAIT_RANGE = range(1, 6) # 1-5 inclusive 

46 

47DEFAULT_TRAIT = 3 # "balanced" 

48 

49 

50# ─── trait descriptors ──────────────────────────────────────────────── 

51 

52 

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} 

60 

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} 

68 

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} 

76 

77_LEVEL_NAMES = {1: "very low", 2: "low", 3: "moderate", 4: "high", 5: "very high"} 

78 

79 

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") 

83 

84 

85# ─── data shapes ────────────────────────────────────────────────────── 

86 

87 

88@dataclass 

89class BankDisposition: 

90 """Three personality traits shaping the bank's answer style.""" 

91 

92 skepticism: int = DEFAULT_TRAIT 

93 literalism: int = DEFAULT_TRAIT 

94 empathy: int = DEFAULT_TRAIT 

95 

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}") 

106 

107 def to_dict(self) -> dict[str, int]: 

108 return asdict(self) 

109 

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 ) 

118 

119 @classmethod 

120 def balanced(cls) -> BankDisposition: 

121 """The default 'no special bias' disposition (3/3/3).""" 

122 return cls() 

123 

124 

125@dataclass 

126class BankProfile: 

127 """A bank's prompt-shaping configuration: disposition + background.""" 

128 

129 disposition: BankDisposition = field(default_factory=BankDisposition.balanced) 

130 background: str = "" 

131 

132 def to_dict(self) -> dict[str, Any]: 

133 return { 

134 "disposition": self.disposition.to_dict(), 

135 "background": self.background, 

136 } 

137 

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 ) 

144 

145 

146# ─── prompt formatters ──────────────────────────────────────────────── 

147 

148 

149def format_disposition_block(disposition: BankDisposition) -> str: 

150 """Render the three traits as a system-prompt block. 

151 

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 ) 

163 

164 

165def format_background_block(background: str) -> str: 

166 """Render the background text as a system-prompt block. 

167 

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}" 

175 

176 

177def format_profile_block(profile: BankProfile) -> str: 

178 """Render disposition + background as a combined system-prompt block. 

179 

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) 

189 

190 

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 )