fix(tasks): read Memory.text in classify_events personal context (#3640)

The classify_events task pulled user memories to give the LLM personal context,
but read `m.content`, which the Memory ORM does not have (the column is `text`).
That raised AttributeError on the first row; the surrounding except swallowed it
and logged at debug, so the personal-context block was silently always empty and
events were classified without it.

Extract the rendering into `_memory_context_lines` (reads `text`, robust via
getattr, keeps the 200-char and 40-line caps) and raise the swallowed-exception
log to warning so a future schema mismatch is visible.

Adds tests/test_classify_events_memory_text.py for the field, truncation, blank
skipping, missing-attr robustness, and the line cap.
This commit is contained in:
Mazen Tamer Salah
2026-06-10 20:03:45 +03:00
committed by GitHub
parent 8bf8212846
commit f5b91f1e9e
2 changed files with 55 additions and 9 deletions
+22 -9
View File
@@ -579,6 +579,24 @@ def _classify_event_heuristic(summary: str) -> tuple:
return etype, None
def _memory_context_lines(mems, limit: int = 40) -> list:
"""Render Memory rows into short personal-context bullets for event classify.
Reads the Memory ORM `text` column. The previous inline code read a
non-existent `content` attribute, so it raised AttributeError on the first
row, the surrounding except swallowed it, and the classifier ran with no
personal context at all. getattr keeps it robust to future schema drift.
"""
lines: list = []
for m in mems:
c = (getattr(m, "text", "") or "").strip()
if c:
lines.append(f"- {c[:200]}")
if len(lines) >= limit:
break
return lines
async def action_classify_events(owner: str, **kwargs) -> Tuple[str, bool]:
"""Hybrid classification of upcoming calendar events: fast heuristic for
obvious cases, LLM fallback for ambiguous ones. Assigns event_type +
@@ -614,16 +632,11 @@ async def action_classify_events(owner: str, **kwargs) -> Tuple[str, bool]:
try:
from core.database import Memory as _Mem
_mems = db.query(_Mem).filter(_Mem.owner == owner).limit(60).all() if owner else []
if _mems:
_lines = []
for m in _mems:
c = (m.content or "").strip()
if c:
_lines.append(f"- {c[:200]}")
if _lines:
_memory_context = "USER CONTEXT (relationships, work, life):\n" + "\n".join(_lines[:40]) + "\n\n"
_lines = _memory_context_lines(_mems)
if _lines:
_memory_context = "USER CONTEXT (relationships, work, life):\n" + "\n".join(_lines) + "\n\n"
except Exception as _me:
logger.debug(f"Could not load memory for classify: {_me}")
logger.warning(f"Could not load memory for classify: {_me}")
classified_h = 0
classified_llm = 0
+33
View File
@@ -0,0 +1,33 @@
"""classify_events must read the Memory `text` column, not a non-existent
`content` attribute.
The previous inline loop did `m.content`, which raised AttributeError on the
first Memory row; the surrounding except swallowed it, so the personal-context
block the LLM relies on was always empty. The logic now lives in
`_memory_context_lines`, which reads `text`.
"""
from src.builtin_actions import _memory_context_lines
class _Mem:
def __init__(self, text):
self.text = text
def test_uses_text_and_truncates_and_skips_blank():
lines = _memory_context_lines([_Mem("Alice is my spouse"), _Mem(" "), _Mem("y" * 250)])
assert lines[0] == "- Alice is my spouse"
assert len(lines) == 2 # the blank row is skipped
assert lines[1] == "- " + "y" * 200 # truncated to 200 chars
def test_skips_rows_without_text_attribute():
class _Bad: # mimics a schema where the attribute is absent
pass
assert _memory_context_lines([_Bad(), _Mem("ok")]) == ["- ok"]
def test_respects_limit():
mems = [_Mem(f"memory {i}") for i in range(50)]
assert len(_memory_context_lines(mems, limit=40)) == 40