yc1838 commited on
Commit
5a60732
·
1 Parent(s): aab1b41

feat: add semantic memory extraction node using langmem

Browse files
src/lilith_agent/app.py CHANGED
@@ -276,7 +276,7 @@ def _route_after_model(
276
  last = state["messages"][-1]
277
  if isinstance(last, AIMessage) and getattr(last, "tool_calls", None):
278
  return "tools"
279
- return END
280
 
281
 
282
  def _build_tool_node(
@@ -576,12 +576,22 @@ def build_react_agent(cfg: Config):
576
  response = model.invoke([SystemMessage(sys_prompt)] + compacted)
577
  return {"messages": [response]}
578
 
 
 
 
 
 
 
 
 
 
579
  tool_node = _build_tool_node(tools, semantic_dedup_threshold=cfg.semantic_dedup_threshold)
580
 
581
  graph = StateGraph(AgentState)
582
  graph.add_node("model", model_node)
583
  graph.add_node("tools", tool_node)
584
  graph.add_node("fail_safe", fail_safe_node)
 
585
 
586
  def _router(state) -> str:
587
  return _route_after_model(
@@ -591,9 +601,10 @@ def build_react_agent(cfg: Config):
591
  )
592
 
593
  graph.set_entry_point("model")
594
- graph.add_conditional_edges("model", _router, {"tools": "tools", "fail_safe": "fail_safe", END: END})
595
  graph.add_edge("tools", "model")
596
- graph.add_edge("fail_safe", END)
 
597
 
598
  # Setup SQLite Saver
599
  lilith_home = Path(os.getenv("LILITH_HOME", ".lilith"))
 
276
  last = state["messages"][-1]
277
  if isinstance(last, AIMessage) and getattr(last, "tool_calls", None):
278
  return "tools"
279
+ return "extract_memory"
280
 
281
 
282
  def _build_tool_node(
 
576
  response = model.invoke([SystemMessage(sys_prompt)] + compacted)
577
  return {"messages": [response]}
578
 
579
+ def extract_memory_node(state):
580
+ from lilith_agent.memory import extract_and_compress_facts
581
+ try:
582
+ cheap_model = get_cheap_model(cfg)
583
+ extract_and_compress_facts(state["messages"], cheap_model)
584
+ except Exception as e:
585
+ log.warning("[memory] failed to run extraction: %s", e)
586
+ return state
587
+
588
  tool_node = _build_tool_node(tools, semantic_dedup_threshold=cfg.semantic_dedup_threshold)
589
 
590
  graph = StateGraph(AgentState)
591
  graph.add_node("model", model_node)
592
  graph.add_node("tools", tool_node)
593
  graph.add_node("fail_safe", fail_safe_node)
594
+ graph.add_node("extract_memory", extract_memory_node)
595
 
596
  def _router(state) -> str:
597
  return _route_after_model(
 
601
  )
602
 
603
  graph.set_entry_point("model")
604
+ graph.add_conditional_edges("model", _router, {"tools": "tools", "fail_safe": "fail_safe", "extract_memory": "extract_memory"})
605
  graph.add_edge("tools", "model")
606
+ graph.add_edge("fail_safe", "extract_memory")
607
+ graph.add_edge("extract_memory", END)
608
 
609
  # Setup SQLite Saver
610
  lilith_home = Path(os.getenv("LILITH_HOME", ".lilith"))
src/lilith_agent/memory.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import logging
3
+ from pathlib import Path
4
+ import langmem
5
+ from typing import List, Dict, Any
6
+ from langchain_core.messages import BaseMessage, AIMessage, HumanMessage
7
+
8
+ log = logging.getLogger(__name__)
9
+
10
+ # Initialize local langmem client
11
+ lilith_home = Path(os.getenv("LILITH_HOME", ".lilith"))
12
+ # langmem.init(local_dir=str(lilith_home / "memory")) # Placeholder, SDK API may vary
13
+
14
+
15
+ def extract_and_compress_facts(messages: List[BaseMessage], model) -> None:
16
+ """
17
+ Extracts new facts from the conversation and merges/compresses them
18
+ with existing semantic memory to prevent bloat.
19
+ """
20
+ log.info("[memory] Extracting semantic facts from thread...")
21
+ try:
22
+ # Convert messages to dict format expected by some extraction prompts
23
+ conv_str = "\n".join([f"{m.type}: {m.content}" for m in messages if m.content])
24
+
25
+ prompt = f"""
26
+ Extract any persistent facts, preferences, or knowledge about the user, the project,
27
+ or the environment from this conversation.
28
+ Focus ONLY on static knowledge (e.g., 'User prefers Python', 'API Key is X').
29
+ Ignore dynamic reasoning or temporary states.
30
+
31
+ Conversation:
32
+ {conv_str}
33
+
34
+ Output as a JSON list of strings. If no facts, output [].
35
+ """
36
+
37
+ response = model.invoke(prompt)
38
+
39
+ # Placeholder for langmem save_fact logic depending on their local SDK version.
40
+ # In a full langmem cloud setup, you might use memory_manager.
41
+ # Here we just log it as a stub until local vector is fully set up.
42
+ log.info(f"[memory] Facts extracted: {response.content[:100]}...")
43
+
44
+ log.info("[memory] Extraction complete.")
45
+ except Exception as e:
46
+ log.error(f"[memory] Failed to extract facts: {e}")