diff --git a/src/builtin_actions.py b/src/builtin_actions.py index b48ed94fa..1ea7cd8a4 100644 --- a/src/builtin_actions.py +++ b/src/builtin_actions.py @@ -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 diff --git a/tests/test_classify_events_memory_text.py b/tests/test_classify_events_memory_text.py new file mode 100644 index 000000000..328929115 --- /dev/null +++ b/tests/test_classify_events_memory_text.py @@ -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