personaflow / app /memory.py
mozzic's picture
Deploy PersonaFlow AI
9d1557c verified
Raw
History Blame Contribute Delete
5.76 kB
"""Behavioral memory: long-term + short-term + contextual state per user.
Long-term: stable preferences derived from full history.
Short-term: mood + explicit recent_experiences tags from the LAST K reviews.
Contextual: situational flags supplied per-request (rainy, salary_week, etc.).
The PRD asks for a memory graph; we model it as a layered dict with explicit
tagged experiences ([late_delivery, bad_packaging, ...]) so downstream agents
can reason over recent friction without re-reading full review text.
"""
from __future__ import annotations
import json
import re
from pathlib import Path
import pandas as pd
from app.config import MEMORY_DIR
from app.persona.features import (
DELIVERY_TERMS,
NEG_TERMS,
PACKAGING_TERMS,
POS_TERMS,
PRICE_TERMS,
QUALITY_TERMS,
SERVICE_TERMS,
)
from app.persona.store import user_reviews
# Tag rules: (tag name, regex/keyword set, rating-condition predicate)
_TAG_RULES: list[tuple[str, set[str], int]] = [
("late_delivery", {"late", "slow", "delay", "took forever", "took too long"}, 3),
("bad_packaging", {"damaged", "leaked", "crushed", "broken", "open", "torn", "smashed"}, 3),
("bad_quality", {"stale", "rotten", "moldy", "spoiled", "rancid", "off"}, 3),
("bad_service", {"rude", "unhelpful", "no response", "refused refund", "no refund"}, 3),
("overpriced", {"overpriced", "not worth", "too expensive", "rip off"}, 3),
("great_value", {"great deal", "worth it", "bargain", "great price"}, 4),
("loved_quality", {"delicious", "perfect", "amazing", "fantastic", "love this"}, 4),
]
def _path(user_id: str) -> Path:
return MEMORY_DIR / f"{user_id.replace('/', '_')}.json"
def _recent_signal(reviews: pd.DataFrame, tokens: set[str]) -> float:
if reviews.empty:
return 0.0
return float(reviews["text"].str.lower().apply(lambda t: any(tok in t for tok in tokens)).mean())
def _tag_review(text: str, rating: int) -> list[str]:
"""Return tags that fire on a single review."""
t = text.lower()
tags = []
for tag, tokens, threshold in _TAG_RULES:
hit = any(tok in t for tok in tokens)
if not hit:
continue
if threshold <= 3 and rating <= threshold:
tags.append(tag)
elif threshold >= 4 and rating >= threshold:
tags.append(tag)
return tags
def build(user_id: str, last_k: int = 5) -> dict:
from app.persona.coldstart import neutral_memory
from app.persona.store import _reviews
df = _reviews()
sub = df[df["user_id"] == user_id]
if sub.empty:
out = neutral_memory(user_id)
_path(user_id).write_text(json.dumps(out, indent=2), encoding="utf-8")
return out
reviews = sub.sort_values("timestamp")
recent = reviews.tail(last_k)
recent_avg = float(recent["rating"].mean())
long_avg = float(reviews["rating"].mean())
drift = recent_avg - long_avg # negative = trending grumpier
recent_frustration = _recent_signal(recent, NEG_TERMS)
recent_joy = _recent_signal(recent, POS_TERMS)
recent_delivery_complaints = _recent_signal(recent[recent["rating"] <= 2], DELIVERY_TERMS)
recent_price_complaints = _recent_signal(recent[recent["rating"] <= 2], PRICE_TERMS)
recent_packaging_complaints = _recent_signal(recent[recent["rating"] <= 2], PACKAGING_TERMS)
recent_service_complaints = _recent_signal(recent[recent["rating"] <= 2], SERVICE_TERMS)
# Explicit per-review experience tags from the recent window
experiences: list[dict] = []
tag_counts: dict[str, int] = {}
for _, row in recent.iterrows():
tags = _tag_review(str(row["text"]), int(row["rating"]))
if not tags:
continue
experiences.append(
{
"timestamp": str(row["timestamp"]),
"rating": int(row["rating"]),
"tags": tags,
"summary": str(row.get("summary") or "")[:80],
}
)
for t in tags:
tag_counts[t] = tag_counts.get(t, 0) + 1
# Open friction = any negative tag fired in recent window
negative_tags = {"late_delivery", "bad_packaging", "bad_quality", "bad_service", "overpriced"}
open_friction = sorted({t for t in tag_counts if t in negative_tags})
mood = "neutral"
if drift <= -0.7 or recent_frustration > 0.4 or open_friction:
mood = "frustrated"
elif drift >= 0.7 or recent_joy > 0.5:
mood = "upbeat"
memory = {
"user_id": user_id,
"long_term": {
"avg_rating": round(long_avg, 3),
"n_reviews_total": int(len(reviews)),
},
"short_term": {
"recent_avg_rating": round(recent_avg, 3),
"rating_drift": round(drift, 3),
"mood": mood,
"recent_frustration": round(recent_frustration, 3),
"recent_joy": round(recent_joy, 3),
"recent_delivery_complaints": round(recent_delivery_complaints, 3),
"recent_price_complaints": round(recent_price_complaints, 3),
"recent_packaging_complaints": round(recent_packaging_complaints, 3),
"recent_service_complaints": round(recent_service_complaints, 3),
"open_friction": open_friction,
"tag_counts": tag_counts,
"recent_experiences": experiences,
"last_k_ratings": recent["rating"].tolist(),
"last_k_summaries": recent["summary"].fillna("").tolist(),
},
}
_path(user_id).write_text(json.dumps(memory, indent=2), encoding="utf-8")
return memory
def get_or_build(user_id: str) -> dict:
p = _path(user_id)
if p.exists():
return json.loads(p.read_text(encoding="utf-8"))
return build(user_id)