Spaces:
Sleeping
Sleeping
yc1838 commited on
Commit ·
5a60732
1
Parent(s): aab1b41
feat: add semantic memory extraction node using langmem
Browse files- src/lilith_agent/app.py +14 -3
- src/lilith_agent/memory.py +46 -0
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
|
| 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",
|
| 595 |
graph.add_edge("tools", "model")
|
| 596 |
-
graph.add_edge("fail_safe",
|
|
|
|
| 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}")
|