yc1838 commited on
Commit
0998adb
·
1 Parent(s): 90d071c

fixed memory

Browse files
package-lock.json ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "name": "lilith-agent",
3
+ "lockfileVersion": 3,
4
+ "requires": true,
5
+ "packages": {}
6
+ }
pyproject.toml CHANGED
@@ -12,7 +12,7 @@ dependencies = [
12
  "langmem>=0.0.1",
13
  "langchain-core>=1.0,<2.0",
14
  "langchain-anthropic>=1.0,<2.0",
15
- "langchain-google-genai>=2.0.0,<5.0",
16
  "langchain-ollama>=1.0,<2.0",
17
  "langchain-huggingface>=1.0,<2.0",
18
  "langchain-openai>=1.0.0,<2.0",
 
12
  "langmem>=0.0.1",
13
  "langchain-core>=1.0,<2.0",
14
  "langchain-anthropic>=1.0,<2.0",
15
+ "langchain-google-genai>=4.2.2,<5.0",
16
  "langchain-ollama>=1.0,<2.0",
17
  "langchain-huggingface>=1.0,<2.0",
18
  "langchain-openai>=1.0.0,<2.0",
src/lilith_agent.egg-info/PKG-INFO CHANGED
@@ -8,7 +8,7 @@ Requires-Dist: langgraph-checkpoint-sqlite
8
  Requires-Dist: langmem>=0.0.1
9
  Requires-Dist: langchain-core<2.0,>=1.0
10
  Requires-Dist: langchain-anthropic<2.0,>=1.0
11
- Requires-Dist: langchain-google-genai<5.0,>=2.0.0
12
  Requires-Dist: langchain-ollama<2.0,>=1.0
13
  Requires-Dist: langchain-huggingface<2.0,>=1.0
14
  Requires-Dist: langchain-openai<2.0,>=1.0.0
 
8
  Requires-Dist: langmem>=0.0.1
9
  Requires-Dist: langchain-core<2.0,>=1.0
10
  Requires-Dist: langchain-anthropic<2.0,>=1.0
11
+ Requires-Dist: langchain-google-genai<5.0,>=4.2.2
12
  Requires-Dist: langchain-ollama<2.0,>=1.0
13
  Requires-Dist: langchain-huggingface<2.0,>=1.0
14
  Requires-Dist: langchain-openai<2.0,>=1.0.0
src/lilith_agent.egg-info/SOURCES.txt CHANGED
@@ -5,6 +5,7 @@ src/lilith_agent/__init__.py
5
  src/lilith_agent/app.py
6
  src/lilith_agent/config.py
7
  src/lilith_agent/gaia_dataset.py
 
8
  src/lilith_agent/models.py
9
  src/lilith_agent/observability.py
10
  src/lilith_agent/runner.py
 
5
  src/lilith_agent/app.py
6
  src/lilith_agent/config.py
7
  src/lilith_agent/gaia_dataset.py
8
+ src/lilith_agent/memory.py
9
  src/lilith_agent/models.py
10
  src/lilith_agent/observability.py
11
  src/lilith_agent/runner.py
src/lilith_agent.egg-info/requires.txt CHANGED
@@ -3,7 +3,7 @@ langgraph-checkpoint-sqlite
3
  langmem>=0.0.1
4
  langchain-core<2.0,>=1.0
5
  langchain-anthropic<2.0,>=1.0
6
- langchain-google-genai<5.0,>=2.0.0
7
  langchain-ollama<2.0,>=1.0
8
  langchain-huggingface<2.0,>=1.0
9
  langchain-openai<2.0,>=1.0.0
 
3
  langmem>=0.0.1
4
  langchain-core<2.0,>=1.0
5
  langchain-anthropic<2.0,>=1.0
6
+ langchain-google-genai<5.0,>=4.2.2
7
  langchain-ollama<2.0,>=1.0
8
  langchain-huggingface<2.0,>=1.0
9
  langchain-openai<2.0,>=1.0.0
src/lilith_agent/memory.py CHANGED
@@ -1,102 +1,199 @@
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 summarize_episode(messages: List[BaseMessage], model) -> None:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  """
17
- Summarizes the trajectory of the task to learn from past experiences.
 
18
  """
19
- log.info("[memory] Summarizing task episode...")
20
  try:
21
- # Extract the initial question
22
- initial_question = ""
23
- for m in messages:
24
- if isinstance(m, HumanMessage):
25
- initial_question = str(m.content)
26
- break
27
-
28
- conv_str = "\n".join([f"{m.type}: {m.content[:200]}..." for m in messages if m.content])
29
 
 
 
 
 
 
 
 
 
30
  prompt = f"""
31
- Summarize the trajectory of this task to help a future agent avoid mistakes and repeat successes.
32
- Include:
33
- 1. Task description
34
- 2. Tools used and why
35
- 3. Errors encountered and how they were bypassed
36
- 4. Final outcome
37
-
38
- Initial Question: {initial_question}
39
- Trajectory:
40
- {conv_str}
41
- """
 
 
 
 
 
 
 
42
 
43
  response = model.invoke(prompt)
 
 
 
44
 
45
- # Placeholder for langmem save_episode logic
46
- log.info(f"[memory] Episode summarized: {response.content[:100]}...")
 
 
 
 
 
 
 
 
 
 
 
47
  except Exception as e:
48
- log.error(f"[memory] Failed to summarize episode: {e}")
49
 
50
- def extract_and_compress_facts(messages: List[BaseMessage], model) -> None:
51
- """
52
- Extracts new facts from the conversation and merges/compresses them
53
- with existing semantic memory to prevent bloat.
54
- """
55
- log.info("[memory] Extracting semantic facts from thread...")
56
  try:
57
- # Convert messages to dict format expected by some extraction prompts
58
- conv_str = "\n".join([f"{m.type}: {m.content}" for m in messages if m.content])
59
-
 
 
 
 
 
 
 
 
 
 
 
60
  prompt = f"""
61
- Extract any persistent facts, preferences, or knowledge about the user, the project,
62
- or the environment from this conversation.
63
- Focus ONLY on static knowledge (e.g., 'User prefers Python', 'API Key is X').
64
- Ignore dynamic reasoning or temporary states.
65
-
66
- Conversation:
67
- {conv_str}
68
-
69
- Output as a JSON list of strings. If no facts, output [].
70
- """
71
-
72
  response = model.invoke(prompt)
73
-
74
- # Placeholder for langmem save_fact logic
75
- log.info(f"[memory] Facts extracted: {response.content[:100]}...")
76
-
77
- log.info("[memory] Extraction complete.")
78
  except Exception as e:
79
- log.error(f"[memory] Failed to extract facts: {e}")
80
-
81
- summarize_episode(messages, model)
82
 
83
  def retrieve_relevant_context(query: str) -> str:
84
- """
85
- Queries the semantic and episodic memory banks for relevant facts and past experiences.
86
- """
87
  try:
88
- # Placeholder for actual langmem SDK sparse retrieval:
89
- # facts = langmem.search_facts(query, top_k=3)
90
- # episodes = langmem.search_episodes(query, top_k=1)
91
-
92
- facts = [] # stub
93
- episodes = [] # stub
94
 
95
  context_parts = []
96
  if facts:
97
- context_parts.append("<relevant_facts>\n" + "\n".join(f"- {f}" for f in facts) + "\n</relevant_facts>")
 
 
98
  if episodes:
99
- context_parts.append("<past_experiences>\n" + "\n".join(f"- {e}" for e in episodes) + "\n</past_experiences>")
 
100
 
101
  return "\n\n".join(context_parts)
102
  except Exception as e:
 
1
  import os
2
+ import json
3
+ import sqlite3
4
  import logging
5
  from pathlib import Path
 
6
  from typing import List, Dict, Any
7
+ from datetime import datetime
8
  from langchain_core.messages import BaseMessage, AIMessage, HumanMessage
9
 
10
  log = logging.getLogger(__name__)
11
 
12
+ # Constants
13
+ LILITH_HOME = Path(os.getenv("LILITH_HOME", ".lilith"))
14
+ MEMORY_DB_PATH = LILITH_HOME / "long_term_memory.sqlite"
15
 
16
+ class MemoryStore:
17
+ def __init__(self, db_path: Path = MEMORY_DB_PATH):
18
+ self.db_path = db_path
19
+ self._init_db()
20
 
21
+ def _init_db(self):
22
+ self.db_path.parent.mkdir(parents=True, exist_ok=True)
23
+ conn = sqlite3.connect(str(self.db_path))
24
+ with conn:
25
+ conn.execute("""
26
+ CREATE TABLE IF NOT EXISTS memories (
27
+ id TEXT PRIMARY KEY,
28
+ content TEXT NOT NULL,
29
+ type TEXT DEFAULT 'fact',
30
+ created_at TEXT NOT NULL,
31
+ updated_at TEXT NOT NULL
32
+ )
33
+ """)
34
+ conn.execute("""
35
+ CREATE TABLE IF NOT EXISTS episodes (
36
+ id TEXT PRIMARY KEY,
37
+ task TEXT NOT NULL,
38
+ summary TEXT NOT NULL,
39
+ outcome TEXT,
40
+ created_at TEXT NOT NULL
41
+ )
42
+ """)
43
+ conn.close()
44
+
45
+ def get_all_memories(self) -> List[Dict[str, Any]]:
46
+ conn = sqlite3.connect(str(self.db_path))
47
+ conn.row_factory = sqlite3.Row
48
+ cur = conn.cursor()
49
+ cur.execute("SELECT * FROM memories ORDER BY updated_at DESC")
50
+ rows = [dict(r) for r in cur.fetchall()]
51
+ conn.close()
52
+ return rows
53
+
54
+ def save_memories(self, memories: List[Dict[str, Any]]):
55
+ """Replaces the memories table with the provided list (active compression)."""
56
+ conn = sqlite3.connect(str(self.db_path))
57
+ with conn:
58
+ conn.execute("DELETE FROM memories")
59
+ for m in memories:
60
+ now = datetime.now().isoformat()
61
+ conn.execute(
62
+ "INSERT INTO memories (id, content, type, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
63
+ (m.get("id", str(hash(m["content"]))), m["content"], m.get("type", "fact"), now, now)
64
+ )
65
+ conn.close()
66
+
67
+ def add_episode(self, task: str, summary: str, outcome: str):
68
+ conn = sqlite3.connect(str(self.db_path))
69
+ with conn:
70
+ now = datetime.now().isoformat()
71
+ conn.execute(
72
+ "INSERT INTO episodes (id, task, summary, outcome, created_at) VALUES (?, ?, ?, ?, ?)",
73
+ (str(hash(task + now)), task, summary, outcome, now)
74
+ )
75
+ conn.close()
76
+
77
+ def get_recent_episodes(self, limit: int = 3) -> List[Dict[str, Any]]:
78
+ conn = sqlite3.connect(str(self.db_path))
79
+ conn.row_factory = sqlite3.Row
80
+ cur = conn.cursor()
81
+ cur.execute("SELECT * FROM episodes ORDER BY created_at DESC LIMIT ?", (limit,))
82
+ rows = [dict(r) for r in cur.fetchall()]
83
+ conn.close()
84
+ return rows
85
+
86
+ _store = MemoryStore()
87
+
88
+ def extract_and_compress_facts(messages: List[BaseMessage], model) -> None:
89
  """
90
+ Extracts new facts and merges them with existing ones using an LLM.
91
+ Implements the 'Engram' / 'HCA' active compression pattern.
92
  """
93
+ log.info("[memory] Running active memory compression...")
94
  try:
95
+ existing_memories = _store.get_all_memories()
96
+ existing_str = json.dumps([m["content"] for m in existing_memories], indent=2, ensure_ascii=False)
 
 
 
 
 
 
97
 
98
+ conv_parts = []
99
+ for m in messages:
100
+ role = "User" if isinstance(m, HumanMessage) else "Assistant"
101
+ if m.content:
102
+ content = m.content if isinstance(m.content, str) else str(m.content)
103
+ conv_parts.append(f"{role}: {content[:1000]}")
104
+ conv_str = "\n".join(conv_parts)
105
+
106
  prompt = f"""
107
+ You are Lilith's Long-Term Memory Manager. Your goal is to maintain a DENSE, ATOMIC, and ACCURATE set of facts about the user and the environment.
108
+
109
+ ### CURRENT MEMORIES:
110
+ {existing_str}
111
+
112
+ ### RECENT CONVERSATION:
113
+ {conv_str}
114
+
115
+ ### TASK:
116
+ 1. Identify any NEW persistent facts, preferences, or entities mentioned in the conversation.
117
+ 2. Update or resolve contradictions with EXISTING memories.
118
+ 3. REMOVE redundant or trivial memories.
119
+ 4. Keep the list concise and focused on high-signal information (e.g., user name, preferences, project details, API keys mentioned).
120
+
121
+ Output the updated list of ALL persistent facts as a JSON array of strings.
122
+ Example: ["User name is Alice", "Project uses Python 3.11"]
123
+ If no changes or facts, return the existing list.
124
+ """
125
 
126
  response = model.invoke(prompt)
127
+ content = response.content
128
+ if isinstance(content, list): # Handle thinking models
129
+ content = content[-1].get("text", "") if isinstance(content[-1], dict) else str(content[-1])
130
 
131
+ # Sane JSON extraction
132
+ try:
133
+ start = content.find("[")
134
+ end = content.rfind("]") + 1
135
+ if start != -1 and end > start:
136
+ facts = json.loads(content[start:end])
137
+ if isinstance(facts, list):
138
+ updated = [{"content": f} for f in facts]
139
+ _store.save_memories(updated)
140
+ log.info(f"[memory] Saved {len(updated)} facts.")
141
+ except Exception as je:
142
+ log.warning(f"[memory] JSON parse failed: {je}")
143
+
144
  except Exception as e:
145
+ log.error(f"[memory] Extraction failed: {e}")
146
 
147
+ def summarize_episode(messages: List[BaseMessage], model) -> None:
148
+ """Summarizes the trajectory to help avoid future mistakes."""
149
+ log.info("[memory] Summarizing task episode...")
 
 
 
150
  try:
151
+ initial_question = ""
152
+ outcome = "success"
153
+ for m in messages:
154
+ if isinstance(m, HumanMessage) and not initial_question:
155
+ initial_question = str(m.content)
156
+ if "ERROR" in str(m.content).upper():
157
+ outcome = "failed/struggled"
158
+
159
+ conv_parts = []
160
+ for m in messages:
161
+ if m.content:
162
+ conv_parts.append(f"{m.type}: {str(m.content)[:200]}...")
163
+ conv_str = "\n".join(conv_parts)
164
+
165
  prompt = f"""
166
+ Summarize this task trajectory for Lilith's 'Episodic Memory'.
167
+ Initial Question: {initial_question}
168
+ Outcome: {outcome}
169
+
170
+ Briefly explain:
171
+ 1. What was the goal?
172
+ 2. What tools worked? What failed?
173
+ 3. What is the 'lesson learned' for next time?
174
+
175
+ Keep it under 150 words.
176
+ """
177
  response = model.invoke(prompt)
178
+ _store.add_episode(initial_question, response.content, outcome)
179
+ log.info("[memory] Episode saved.")
 
 
 
180
  except Exception as e:
181
+ log.error(f"[memory] Summarization failed: {e}")
 
 
182
 
183
  def retrieve_relevant_context(query: str) -> str:
184
+ """Fetches all facts and recent episodes to inject into the prompt."""
 
 
185
  try:
186
+ facts = _store.get_all_memories()
187
+ episodes = _store.get_recent_episodes(limit=2)
 
 
 
 
188
 
189
  context_parts = []
190
  if facts:
191
+ fact_lines = "\n".join([f"- {m['content']}" for m in facts])
192
+ context_parts.append(f"<known_facts>\n{fact_lines}\n</known_facts>")
193
+
194
  if episodes:
195
+ epi_lines = "\n\n".join([f"Task: {e['task']}\nSummary: {e['summary']}" for e in episodes])
196
+ context_parts.append(f"<past_experiences>\n{epi_lines}\n</past_experiences>")
197
 
198
  return "\n\n".join(context_parts)
199
  except Exception as e:
test_memory_pick_up.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from lilith_agent.memory import _store, retrieve_relevant_context
3
+
4
+ # Initialize DB via _store
5
+ _store._init_db()
6
+
7
+ # Manually inject
8
+ from datetime import datetime
9
+ import sqlite3
10
+ conn = sqlite3.connect(str(_store.db_path))
11
+ with conn:
12
+ conn.execute("DELETE FROM memories")
13
+ now = datetime.now().isoformat()
14
+ conn.execute(
15
+ "INSERT INTO memories (id, content, type, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
16
+ ("test-fact-1", "The user's secret code name is 'Blue Butterfly'.", "fact", now, now)
17
+ )
18
+ conn.close()
19
+
20
+ context = retrieve_relevant_context("Who am I?")
21
+ print("\nRetrieved Context:")
22
+ print(context)