Coverage for /home/admin/Documents/AI/applications/lexigram-dev/lexigram/experimental/ai/lexigram-ai-memory/src/lexigram/ai/memory/working/manager.py: 30%

66 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-25 07:19 +0800

1"""Working memory manager — assembles token-budgeted context windows.""" 

2 

3from __future__ import annotations 

4 

5from lexigram.ai.memory.config import WorkingMemoryConfig 

6from lexigram.ai.memory.working.token_budget import TokenBudgetAllocator 

7from lexigram.contracts.ai.memory import ( 

8 EpisodicMemoryProtocol, 

9 MemoryEntry, 

10 MemoryQuery, 

11 SemanticMemoryProtocol, 

12) 

13from lexigram.contracts.core.health import HealthCheckResult, HealthStatus 

14from lexigram.logging import ( 

15 get_logger, 

16) 

17 

18logger = get_logger(__name__) 

19 

20 

21class WorkingMemoryManager: 

22 """Assembles the optimal context window from all memory tiers. 

23 

24 Pulls recent turns from episodic memory and relevant facts from 

25 semantic memory, fitting everything within a configurable token budget. 

26 """ 

27 

28 def __init__( 

29 self, 

30 episodic: EpisodicMemoryProtocol | None = None, 

31 semantic: SemanticMemoryProtocol | None = None, 

32 config: WorkingMemoryConfig | None = None, 

33 ) -> None: 

34 """Initialise the working memory manager. 

35 

36 Args: 

37 episodic: Episodic memory source for past interactions. 

38 semantic: Semantic memory source for structured facts. 

39 config: Token budget configuration. 

40 """ 

41 self._episodic = episodic 

42 self._semantic = semantic 

43 self._config = config or WorkingMemoryConfig() 

44 self._allocator = TokenBudgetAllocator(self._config) 

45 self._current_entries: list[MemoryEntry] = [] 

46 

47 async def assemble( 

48 self, 

49 query: str, 

50 token_budget: int, 

51 *, 

52 owner_id: str, 

53 session_id: str | None = None, 

54 ) -> list[MemoryEntry]: 

55 """Assemble context window from all available memory tiers. 

56 

57 Args: 

58 query: Current user query used to retrieve relevant memories. 

59 token_budget: Total token budget for the assembled context. 

60 owner_id: Owner scope restricting memory retrieval. 

61 session_id: Optional session scope for retrieval filtering. 

62 

63 Returns: 

64 Ordered list of memory entries ready for LLM context injection. 

65 """ 

66 budget = self._allocator.allocate(token_budget) 

67 entries: list[MemoryEntry] = [] 

68 

69 if self._episodic: 

70 filters: dict = {} 

71 if session_id: 

72 filters["session_id"] = session_id 

73 

74 # Recent conversation turns 

75 recent = await self._episodic.recall( 

76 MemoryQuery( 

77 owner_id=owner_id, 

78 query=query, 

79 top_k=self._config.max_recent_turns, 

80 recency_weight=0.8, 

81 importance_weight=0.1, 

82 relevance_weight=0.1, 

83 filters={**filters, "type": "turn"} 

84 if filters 

85 else {"type": "turn"}, 

86 ) 

87 ) 

88 # Episodic semantic recall 

89 episodic_results = await self._episodic.recall( 

90 MemoryQuery( 

91 owner_id=owner_id, 

92 query=query, 

93 top_k=10, 

94 recency_weight=0.2, 

95 importance_weight=0.3, 

96 relevance_weight=0.5, 

97 filters=filters, 

98 ) 

99 ) 

100 

101 # Merge: recent turns first, then semantic episodic hits 

102 seen_ids: set[str] = set() 

103 for result in recent: 

104 if result.entry.id not in seen_ids: 

105 entries.append(result.entry) 

106 seen_ids.add(result.entry.id) 

107 for result in episodic_results: 

108 if result.entry.id not in seen_ids: 

109 entries.append(result.entry) 

110 seen_ids.add(result.entry.id) 

111 

112 logger.debug( 

113 "episodic_assembled", 

114 count=len(entries), 

115 budget=budget["episodic"] + budget["recent_turns"], 

116 ) 

117 

118 if self._semantic: 

119 # Inject top semantic facts as synthetic memory entries 

120 from datetime import UTC, datetime 

121 from uuid import uuid4 

122 

123 facts = await self._semantic.query_facts(subject=query[:100]) 

124 for fact in facts[: budget["semantic"] // 20]: # rough token estimate 

125 fact_entry = MemoryEntry( 

126 id=str(uuid4()), 

127 owner_id=owner_id, 

128 content=f"{fact.get('subject')} {fact.get('predicate')} {fact.get('object_')}", 

129 role="system", 

130 timestamp=datetime.now(UTC), 

131 importance=fact.get("confidence", 0.5), 

132 metadata={"source": "semantic", **fact}, 

133 ) 

134 entries.append(fact_entry) 

135 

136 logger.debug("semantic_injected", count=len(facts)) 

137 

138 self._current_entries = entries 

139 return entries 

140 

141 async def add(self, entry: MemoryEntry) -> None: 

142 """Add an entry to the current working memory stream. 

143 

144 Args: 

145 entry: Memory entry to add. 

146 """ 

147 self._current_entries.append(entry) 

148 if self._episodic: 

149 await self._episodic.record(entry) 

150 

151 async def get_context_entries(self) -> list[MemoryEntry]: 

152 """Return the entries currently assembled in working context. 

153 

154 Returns: 

155 Current context entry list. 

156 """ 

157 return list(self._current_entries) 

158 

159 async def flush(self) -> None: 

160 """Clear the current context assembly.""" 

161 self._current_entries.clear() 

162 

163 async def health_check(self, timeout: float = 5.0) -> HealthCheckResult: 

164 """Check health of underlying memory tiers. 

165 

166 Args: 

167 timeout: Maximum seconds for the health check. 

168 

169 Returns: 

170 HealthCheckResult aggregating episodic and semantic tier health. 

171 """ 

172 try: 

173 ep_healthy = True 

174 sem_healthy = True 

175 if self._episodic: 

176 ep_result = await self._episodic.health_check(timeout) 

177 ep_healthy = ep_result.status == HealthStatus.HEALTHY 

178 if self._semantic: 

179 sem_result = await self._semantic.health_check(timeout) 

180 sem_healthy = sem_result.status == HealthStatus.HEALTHY 

181 status = ( 

182 HealthStatus.HEALTHY 

183 if ep_healthy and sem_healthy 

184 else HealthStatus.DEGRADED 

185 ) 

186 return HealthCheckResult(component="working_memory", status=status) 

187 except Exception as exc: # noqa: BLE001 

188 return HealthCheckResult( 

189 component="working_memory", 

190 status=HealthStatus.UNHEALTHY, 

191 message=str(exc), 

192 ) 

193 

194 

195__all__ = ["WorkingMemoryManager"]