/ core / altitude / complementary.py
complementary.py
  1  """
  2  Complementary Altitude Engine
  3  
  4  Implements the key principle: the system should operate at the
  5  OPPOSITE altitude from the operator.
  6  
  7  When they're abstract (philosophical), ground them in concrete.
  8  When they're concrete (operational), lift them to principles.
  9  
 10  This creates productive tension that:
 11  - Prevents getting lost in abstraction
 12  - Prevents getting lost in weeds
 13  - Surfaces relevant connections across altitudes
 14  - Helps operator see full picture
 15  """
 16  
 17  from dataclasses import dataclass, field
 18  from datetime import datetime, timedelta
 19  from typing import Optional, List, Dict, Any, Tuple
 20  from enum import Enum
 21  
 22  from .detector import Altitude, AltitudeDetector, AltitudeDetection, OperatorAltitudeState
 23  from ..database import GodDatabase, Bullet
 24  
 25  
 26  class ComplementStrategy(Enum):
 27      """Strategy for complementary altitude selection."""
 28      DIRECT_OPPOSITE = "direct_opposite"    # Always opposite altitude
 29      ADJACENT_PULL = "adjacent_pull"        # One step toward opposite
 30      BRACKETING = "bracketing"              # Show both higher and lower
 31      GROUNDING = "grounding"                # Always pull toward operational
 32      LIFTING = "lifting"                    # Always pull toward philosophical
 33  
 34  
 35  @dataclass
 36  class ComplementaryRecommendation:
 37      """
 38      A recommendation for complementary content at a different altitude.
 39      """
 40      bullet_uuid: str
 41      bullet_altitude: Altitude
 42      relevance_score: float          # How relevant to current context
 43      altitude_gap: int               # Distance from operator altitude (1-3)
 44      recommendation_reason: str
 45      generated_at: datetime = field(default_factory=datetime.now)
 46  
 47  
 48  class ComplementaryAltitudeEngine:
 49      """
 50      Generates complementary altitude recommendations.
 51  
 52      When operator is at one altitude, surfaces relevant content
 53      from complementary altitudes to provide balance.
 54      """
 55  
 56      def __init__(
 57          self,
 58          god_db: GodDatabase,
 59          detector: Optional[AltitudeDetector] = None,
 60          strategy: ComplementStrategy = ComplementStrategy.DIRECT_OPPOSITE
 61      ):
 62          """
 63          Initialize the complementary altitude engine.
 64  
 65          Args:
 66              god_db: The God Database instance
 67              detector: Optional altitude detector (creates default if not provided)
 68              strategy: Strategy for selecting complementary altitude
 69          """
 70          self.god_db = god_db
 71          self.detector = detector or AltitudeDetector()
 72          self.strategy = strategy
 73  
 74      def get_complementary_altitude(
 75          self,
 76          operator_altitude: Altitude,
 77          strategy: Optional[ComplementStrategy] = None
 78      ) -> List[Altitude]:
 79          """
 80          Get complementary altitude(s) for operator's current altitude.
 81  
 82          Args:
 83              operator_altitude: Operator's current altitude
 84              strategy: Optional override strategy
 85  
 86          Returns:
 87              List of complementary altitudes to surface content from
 88          """
 89          strategy = strategy or self.strategy
 90  
 91          if strategy == ComplementStrategy.DIRECT_OPPOSITE:
 92              # Direct opposite on the altitude ladder
 93              opposites = {
 94                  Altitude.OPERATIONAL: [Altitude.PHILOSOPHICAL],
 95                  Altitude.TACTICAL: [Altitude.STRATEGIC],
 96                  Altitude.STRATEGIC: [Altitude.TACTICAL],
 97                  Altitude.PHILOSOPHICAL: [Altitude.OPERATIONAL],
 98              }
 99              return opposites[operator_altitude]
100  
101          elif strategy == ComplementStrategy.ADJACENT_PULL:
102              # One step toward opposite (gentler transition)
103              adjacent = {
104                  Altitude.OPERATIONAL: [Altitude.TACTICAL],
105                  Altitude.TACTICAL: [Altitude.OPERATIONAL, Altitude.STRATEGIC],
106                  Altitude.STRATEGIC: [Altitude.TACTICAL, Altitude.PHILOSOPHICAL],
107                  Altitude.PHILOSOPHICAL: [Altitude.STRATEGIC],
108              }
109              return adjacent[operator_altitude]
110  
111          elif strategy == ComplementStrategy.BRACKETING:
112              # Both higher and lower (full context)
113              if operator_altitude == Altitude.OPERATIONAL:
114                  return [Altitude.TACTICAL, Altitude.STRATEGIC]
115              elif operator_altitude == Altitude.PHILOSOPHICAL:
116                  return [Altitude.STRATEGIC, Altitude.TACTICAL]
117              else:
118                  # Middle altitudes get both directions
119                  lower = Altitude(operator_altitude.value - 1)
120                  higher = Altitude(operator_altitude.value + 1)
121                  return [lower, higher]
122  
123          elif strategy == ComplementStrategy.GROUNDING:
124              # Always pull toward operational
125              if operator_altitude == Altitude.OPERATIONAL:
126                  return [Altitude.OPERATIONAL]  # Already grounded
127              return [Altitude(operator_altitude.value - 1)]
128  
129          elif strategy == ComplementStrategy.LIFTING:
130              # Always pull toward philosophical
131              if operator_altitude == Altitude.PHILOSOPHICAL:
132                  return [Altitude.PHILOSOPHICAL]  # Already lifted
133              return [Altitude(operator_altitude.value + 1)]
134  
135          return [Altitude.TACTICAL]  # Default fallback
136  
137      def get_complementary_bullets(
138          self,
139          operator_altitude: Altitude,
140          blanket_id: str,
141          current_topic: Optional[str] = None,
142          limit: int = 5
143      ) -> List[ComplementaryRecommendation]:
144          """
145          Get bullets from complementary altitudes.
146  
147          Args:
148              operator_altitude: Operator's current altitude
149              blanket_id: Which Markov blanket to search
150              current_topic: Optional topic for relevance filtering
151              limit: Maximum recommendations to return
152  
153          Returns:
154              List of ComplementaryRecommendation
155          """
156          target_altitudes = self.get_complementary_altitude(operator_altitude)
157  
158          # Get bullets from blanket
159          bullets = self.god_db.get_bullets_for_blanket(blanket_id)
160  
161          recommendations: List[ComplementaryRecommendation] = []
162  
163          for bullet in bullets:
164              # Detect bullet's altitude
165              detection = self.detector.detect(
166                  bullet.content,
167                  content_id=bullet.uuid,
168                  tags=bullet.visible_tags
169              )
170  
171              # Check if bullet is at a target altitude
172              if detection.primary_altitude not in target_altitudes:
173                  continue
174  
175              # Compute altitude gap
176              gap = abs(detection.primary_altitude.value - operator_altitude.value)
177  
178              # Compute topic relevance
179              relevance = 0.5  # Default
180              if current_topic:
181                  if current_topic.lower() in bullet.content.lower():
182                      relevance = 1.0
183                  elif any(current_topic.lower() in tag.lower() for tag in bullet.visible_tags):
184                      relevance = 0.8
185  
186              # Boost by resonance score if available
187              relevance = relevance * 0.7 + bullet.resonance_score * 0.3
188  
189              # Generate reason
190              reason = self._generate_reason(
191                  operator_altitude,
192                  detection.primary_altitude,
193                  gap
194              )
195  
196              recommendations.append(ComplementaryRecommendation(
197                  bullet_uuid=bullet.uuid,
198                  bullet_altitude=detection.primary_altitude,
199                  relevance_score=relevance,
200                  altitude_gap=gap,
201                  recommendation_reason=reason
202              ))
203  
204          # Sort by relevance and return top
205          recommendations.sort(key=lambda r: r.relevance_score, reverse=True)
206          return recommendations[:limit]
207  
208      def _generate_reason(
209          self,
210          operator_altitude: Altitude,
211          bullet_altitude: Altitude,
212          gap: int
213      ) -> str:
214          """Generate human-readable reason for recommendation."""
215          if operator_altitude.value > bullet_altitude.value:
216              # Operator is abstract, grounding them
217              return f"Grounding: You're thinking at {operator_altitude.name.lower()} level. Here's a {bullet_altitude.name.lower()} perspective."
218          else:
219              # Operator is concrete, lifting them
220              return f"Lifting: You're working at {operator_altitude.name.lower()} level. Here's a {bullet_altitude.name.lower()} perspective."
221  
222      def analyze_altitude_balance(
223          self,
224          operator_state: OperatorAltitudeState,
225          threshold_minutes: int = 30
226      ) -> Dict[str, Any]:
227          """
228          Analyze if operator needs altitude balancing.
229  
230          Returns:
231              Analysis dict with recommendations
232          """
233          analysis = {
234              'operator_id': operator_state.operator_id,
235              'current_altitude': operator_state.current_altitude.name,
236              'needs_balancing': False,
237              'recommended_direction': None,
238              'reason': None,
239          }
240  
241          # Check time at current altitude
242          current_time = datetime.now()
243          if operator_state.last_transition:
244              time_at_current = current_time - operator_state.last_transition
245  
246              if time_at_current > timedelta(minutes=threshold_minutes):
247                  analysis['needs_balancing'] = True
248  
249                  if operator_state.current_altitude.value >= 3:
250                      # Too long at high altitude
251                      analysis['recommended_direction'] = 'down'
252                      analysis['reason'] = f"You've been thinking abstractly for {int(time_at_current.total_seconds() / 60)} minutes. Consider grounding in specifics."
253                  else:
254                      # Too long at low altitude
255                      analysis['recommended_direction'] = 'up'
256                      analysis['reason'] = f"You've been in the weeds for {int(time_at_current.total_seconds() / 60)} minutes. Consider stepping back to see patterns."
257  
258          # Check dominant altitude over history
259          dominant = operator_state.get_dominant_altitude()
260          if dominant != operator_state.current_altitude:
261              analysis['dominant_altitude'] = dominant.name
262  
263          return analysis
264  
265      def generate_altitude_prompt(
266          self,
267          operator_altitude: Altitude,
268          complementary_bullets: List[ComplementaryRecommendation]
269      ) -> str:
270          """
271          Generate a prompt/nudge for altitude awareness.
272  
273          Args:
274              operator_altitude: Current operator altitude
275              complementary_bullets: Recommendations from other altitudes
276  
277          Returns:
278              Human-readable prompt
279          """
280          if not complementary_bullets:
281              return ""
282  
283          # Group by altitude
284          by_altitude: Dict[Altitude, List[ComplementaryRecommendation]] = {}
285          for rec in complementary_bullets:
286              if rec.bullet_altitude not in by_altitude:
287                  by_altitude[rec.bullet_altitude] = []
288              by_altitude[rec.bullet_altitude].append(rec)
289  
290          lines = []
291  
292          if operator_altitude.value >= 3:
293              # High altitude - offer grounding
294              lines.append("While you're thinking big picture...")
295              for alt in [Altitude.OPERATIONAL, Altitude.TACTICAL]:
296                  if alt in by_altitude:
297                      count = len(by_altitude[alt])
298                      lines.append(f"  • {count} {alt.name.lower()} items might ground this")
299          else:
300              # Low altitude - offer lifting
301              lines.append("While you're in the details...")
302              for alt in [Altitude.STRATEGIC, Altitude.PHILOSOPHICAL]:
303                  if alt in by_altitude:
304                      count = len(by_altitude[alt])
305                      lines.append(f"  • {count} {alt.name.lower()} items might illuminate this")
306  
307          return "\n".join(lines)
308  
309  
310  # Quick test
311  if __name__ == "__main__":
312      import tempfile
313      import os
314  
315      # Create temp database
316      db_path = os.path.join(tempfile.gettempdir(), "test_complementary.db")
317      db = GodDatabase(db_path)
318  
319      print("=== Complementary Altitude Test ===\n")
320  
321      # Create test bullets at different altitudes
322      bullets_data = [
323          ("Truth requires both honesty and courage", ["#principle"]),
324          ("Our architecture follows domain-driven design", ["#architecture"]),
325          ("Should we use Redis or Memcached for caching?", ["#decision"]),
326          ("Implement the user authentication endpoint", ["#task"]),
327          ("The meaning of security is maintaining trust", ["#philosophy"]),
328          ("Our caching strategy needs three layers", ["#strategy"]),
329          ("Deploy to staging by end of day", ["#action"]),
330      ]
331  
332      for content, tags in bullets_data:
333          db.create_bullet(
334              content=content,
335              blanket_id="test",
336              visible_tags=tags
337          )
338  
339      # Create engine
340      engine = ComplementaryAltitudeEngine(db)
341      detector = AltitudeDetector()
342  
343      # Test with operator at different altitudes
344      print("Testing complementary recommendations:\n")
345  
346      for operator_alt in Altitude:
347          print(f"Operator at {operator_alt.name}:")
348          complementary = engine.get_complementary_altitude(operator_alt)
349          print(f"  Complementary altitudes: {[a.name for a in complementary]}")
350  
351          recs = engine.get_complementary_bullets(
352              operator_altitude=operator_alt,
353              blanket_id="test",
354              limit=3
355          )
356  
357          for rec in recs:
358              bullet = db.get_bullet(rec.bullet_uuid)
359              print(f"  • [{rec.bullet_altitude.name}] {bullet.content[:40]}...")
360              print(f"    Reason: {rec.recommendation_reason}")
361          print()
362  
363      # Test altitude balance analysis
364      print("=== Altitude Balance Analysis ===\n")
365  
366      state = OperatorAltitudeState(
367          operator_id="rick",
368          current_altitude=Altitude.PHILOSOPHICAL
369      )
370      state.last_transition = datetime.now() - timedelta(minutes=45)
371  
372      analysis = engine.analyze_altitude_balance(state)
373      print(f"Current: {analysis['current_altitude']}")
374      print(f"Needs balancing: {analysis['needs_balancing']}")
375      if analysis['reason']:
376          print(f"Reason: {analysis['reason']}")
377  
378      # Cleanup
379      os.remove(db_path)
380      print("\n=== Test Complete ===")