structured memory: facts/pinned/episodes, batched consolidation, participant-scoped recall
This commit is contained in:
@@ -12,6 +12,7 @@ from pathlib import Path
|
||||
from pprint import pformat
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
from .memory import MemoryManager
|
||||
from .persistence import PersistentStore
|
||||
|
||||
|
||||
@@ -150,6 +151,7 @@ class AIResponder(AIResponderBase):
|
||||
stored_memory = self.store.load_memory(self.channel)
|
||||
if stored_memory is not None:
|
||||
self.memory = stored_memory
|
||||
self.memory_manager = MemoryManager(self.store, lambda: self.config, self.consolidate, self.channel)
|
||||
logging.info(f"memmory:\n{self.memory}")
|
||||
|
||||
def message(self, message: AIMessage, limit: Optional[int] = None) -> List[Dict[str, Any]]:
|
||||
@@ -161,7 +163,8 @@ class AIResponder(AIResponderBase):
|
||||
with open(news_feed) as fd:
|
||||
news_feed = fd.read().strip()
|
||||
system = system.replace("{news}", sanitize_external_text(news_feed))
|
||||
system = system.replace("{memory}", self.memory)
|
||||
participants = [message.user] + [entry_user for entry_user in self._history_users(20)]
|
||||
system = system.replace("{memory}", self.memory_manager.memory_block(participants, self.memory))
|
||||
messages.append({"role": "system", "content": system})
|
||||
if limit is not None:
|
||||
while len(self.history) > limit:
|
||||
@@ -232,7 +235,7 @@ class AIResponder(AIResponderBase):
|
||||
async def chat(self, messages: List[Dict[str, Any]], limit: int) -> Tuple[Optional[Dict[str, Any]], int]:
|
||||
raise NotImplementedError()
|
||||
|
||||
async def memory_rewrite(self, memory: str, message_user: str, answer_user: str, question: str, answer: str) -> str:
|
||||
async def consolidate(self, observations: List[Dict[str, Any]], known_facts: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
||||
raise NotImplementedError()
|
||||
|
||||
async def translate(self, text: str, language: str = "english") -> str:
|
||||
@@ -245,6 +248,17 @@ class AIResponder(AIResponderBase):
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _history_users(self, tail: int) -> List[str]:
|
||||
users = []
|
||||
for item in self.history[-tail:]:
|
||||
try:
|
||||
user = parse_json(item["content"]).get("user")
|
||||
except Exception:
|
||||
user = None
|
||||
if user:
|
||||
users.append(str(user))
|
||||
return users
|
||||
|
||||
def shrink_history_by_one(self) -> None:
|
||||
if not self.history:
|
||||
return
|
||||
@@ -268,17 +282,10 @@ class AIResponder(AIResponderBase):
|
||||
while len(self.history) > limit:
|
||||
self.shrink_history_by_one()
|
||||
|
||||
def update_memory(self, memory) -> None:
|
||||
self.memory = memory
|
||||
|
||||
async def _persist_history(self) -> None:
|
||||
if self.store is not None:
|
||||
await asyncio.to_thread(self.store.save_history, self.channel, list(self.history))
|
||||
|
||||
async def _persist_memory(self) -> None:
|
||||
if self.store is not None:
|
||||
await asyncio.to_thread(self.store.save_memory, self.channel, self.memory)
|
||||
|
||||
async def handle_picture(self, response: Dict) -> bool:
|
||||
if not isinstance(response.get("picture"), (type(None), str)):
|
||||
logging.warning(f"picture key is wrong in response: {pp(response)}")
|
||||
@@ -296,16 +303,9 @@ class AIResponder(AIResponderBase):
|
||||
logging.error(f"failed to parse the answer: {pp(err)}\n{repr(answer['content'])}")
|
||||
return None
|
||||
|
||||
async def memoize(self, message_user: str, answer_user: str, message: str, answer: str) -> None:
|
||||
self.memory = await self.memory_rewrite(self.memory, message_user, answer_user, message, answer)
|
||||
self.update_memory(self.memory)
|
||||
await self._persist_memory()
|
||||
|
||||
async def memoize_reaction(self, message_user: str, reaction_user: str, operation: str, reaction: str, message: str) -> None:
|
||||
quoted_message = message.replace("\n", "\n> ")
|
||||
await self.memoize(
|
||||
message_user, "assistant", f"\n> {quoted_message}", f"User {reaction_user} has {operation} this raction: {reaction}"
|
||||
)
|
||||
async def observe_event(self, user: str, kind: str, content: str) -> None:
|
||||
"""Feed a Discord event into the observation stream (MEM-01)."""
|
||||
await self.memory_manager.observe(user, kind, content)
|
||||
|
||||
async def send(self, message: AIMessage) -> AIResponse:
|
||||
# Get the history limit from the configuration
|
||||
@@ -354,9 +354,10 @@ class AIResponder(AIResponderBase):
|
||||
await self._persist_history()
|
||||
logging.info(f"got this answer:\n{str(answer_message)}")
|
||||
|
||||
# Update memory
|
||||
# Feed the observation stream — consolidation is batched (MEM-01/02)
|
||||
await self.observe_event(message.user, "message", message.message)
|
||||
if answer_message.answer is not None:
|
||||
await self.memoize(message.user, "assistant", message.message, answer_message.answer)
|
||||
await self.observe_event("assistant", "message", answer_message.answer)
|
||||
|
||||
# Return the updated answer message
|
||||
return answer_message
|
||||
|
||||
Reference in New Issue
Block a user