lifeos-agent / agents /memory.py
Dhanushkumarps
initial commit: LifeOS environment and Streamlit UI
13ceb89
Raw
History Blame Contribute Delete
6.81 kB
"""
LifeOS — Memory Agent (File 7 of 15)
Stores past scheduling sessions and generates actionable insights
for the Planner via Groq LLM or a deterministic fallback.
Key design points:
- insights_cache is invalidated on every store() call
- retrieve() returns the cache if still valid (no redundant LLM calls)
- _generate_insights() is only called when cache is empty
- All Groq calls use os.getenv("GROQ_API_KEY") — never hardcoded
"""
from __future__ import annotations
import json
import os
from typing import Any, Dict, List
from dotenv import load_dotenv
load_dotenv()
try:
from groq import Groq
_HAS_GROQ = True
except ImportError:
_HAS_GROQ = False
from utils.prompts import MEMORY_RETRIEVAL_PROMPT
class MemoryAgent:
"""
Manages per-episode session history and LLM-powered scheduling insights.
Attributes
----------
sessions : list of stored session dicts
insights_cache : cached plain-text insights string (cleared on store)
"""
def __init__(self) -> None:
self.sessions: List[Dict[str, Any]] = []
self.insights_cache: str = ""
# ------------------------------------------------------------------
# Public API
# ------------------------------------------------------------------
def store(
self,
iteration: int,
plan: Dict[str, Any],
issues: List[str],
reward: float,
) -> None:
"""
Append one session record and invalidate the insights cache.
The cache is cleared so the next retrieve() generates fresh insights
that incorporate this new session.
"""
self.sessions.append({
"iteration": iteration,
"reward": reward,
"issue_count": len(issues),
"issues": issues,
"plan_summary": self._summarize_plan(plan),
})
self.insights_cache = "" # force fresh insights on next retrieve()
print(f"[Memory] Stored session {iteration}: reward={reward:.1f}, issues={len(issues)}")
def retrieve(self) -> str:
"""
Return insights string.
Returns cached value if available (avoids redundant LLM calls).
Generates fresh insights only when cache is empty.
"""
if not self.sessions:
return "- No history yet. Use default scheduling principles."
if self.insights_cache:
print("[Memory] Returning cached insights.")
return self.insights_cache
print("[Memory] Cache empty — generating fresh insights.")
self.insights_cache = self._generate_insights()
return self.insights_cache
# ------------------------------------------------------------------
# Internal helpers
# ------------------------------------------------------------------
def _generate_insights(self) -> str:
"""
Format MEMORY_RETRIEVAL_PROMPT and call Groq.
Falls back to _deterministic_insights() on any failure.
"""
history_json = json.dumps(self.sessions, indent=2)
prompt = MEMORY_RETRIEVAL_PROMPT.format(history=history_json)
if not _HAS_GROQ:
return self._deterministic_insights()
api_key = os.getenv("GROQ_API_KEY")
if not api_key:
return self._deterministic_insights()
try:
client = Groq(api_key=api_key)
response = client.chat.completions.create(
model="llama-3.1-8b-instant",
temperature=0.5,
messages=[
{"role": "system", "content": "You are LifeOS Memory Agent. Return plain text bullet points only."},
{"role": "user", "content": prompt},
],
)
result = (response.choices[0].message.content or "").strip()
if result:
print("[Memory] LLM insights generated successfully.")
return result
return self._deterministic_insights()
except Exception as e:
print(f"[Memory] LLM insight generation failed ({type(e).__name__}): {e}. Using deterministic fallback.")
return self._deterministic_insights()
def _deterministic_insights(self) -> str:
"""Produce bullet-point insights without any LLM call."""
if not self.sessions:
return "- No history yet. Use default scheduling principles."
lines: List[str] = []
rewards = [s["reward"] for s in self.sessions]
best = max(self.sessions, key=lambda s: s["reward"])
worst = min(self.sessions, key=lambda s: s["reward"])
lines.append(
f"- Pattern: Best reward {best['reward']:.1f} at iteration {best['iteration']} "
f"({best['plan_summary']}) -> Advice: Replicate this structure."
)
worst_issues = "; ".join(worst["issues"][:2]) or "none"
lines.append(
f"- Pattern: Worst iteration had issues: {worst_issues} "
f"-> Advice: Directly address these in the next plan."
)
# Recurring issues
all_issues: List[str] = [iss for s in self.sessions for iss in s["issues"]]
from collections import Counter
common = Counter(all_issues).most_common(2)
if common:
lines.append(
f"- Pattern: Recurring issues: {'; '.join(i for i, _ in common)} "
"-> Advice: Fix these first before optimizing elsewhere."
)
if len(rewards) >= 2:
trend = "improving" if rewards[-1] > rewards[0] else "declining"
lines.append(
f"- Pattern: Reward trend is {trend} ({rewards[0]:.1f} -> {rewards[-1]:.1f}) "
"-> Advice: Continue the current improvement strategy."
)
lines.append(
"- Pattern: Always — include breaks every 90 mins and at least one revision slot "
"-> Advice: Never omit these — they give +5 and +2 reward each."
)
return "\n".join(lines[:5])
def _summarize_plan(self, plan: Dict[str, Any]) -> str:
"""Return a short human-readable summary string of a plan dict."""
schedule = plan.get("schedule", {})
n_days = len([d for d, s in schedule.items() if s])
study_tasks = {
slot.get("task")
for slots in schedule.values()
for slot in (slots if isinstance(slots, list) else [])
if slot.get("type") == "study"
}
n_breaks = sum(
1 for slots in schedule.values()
for slot in (slots if isinstance(slots, list) else [])
if slot.get("type") == "break"
)
return f"{len(study_tasks)} tasks, {n_days} days, {n_breaks} breaks"