memlint 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ # Optional: only needed if use_llm=True
2
+ OPENAI_API_KEY=your_key_here
3
+ ANTHROPIC_API_KEY=your_key_here
@@ -0,0 +1,19 @@
1
+ # local dev tooling
2
+ .claude/
3
+ CLAUDE.md
4
+ docs/superpowers/
5
+ stale-detector-spec.md
6
+
7
+ # Python
8
+ __pycache__/
9
+ *.pyc
10
+ *.pyo
11
+ .coverage
12
+ htmlcov/
13
+ .pytest_cache/
14
+ *.egg-info/
15
+ dist/
16
+ build/
17
+
18
+ # Env
19
+ .env
memlint-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MatrixEscaper
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
memlint-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,152 @@
1
+ Metadata-Version: 2.4
2
+ Name: memlint
3
+ Version: 0.1.0
4
+ Summary: Detect stale facts in LLM agent memory stores
5
+ Project-URL: Homepage, https://github.com/Bhavye2003Developer/memlint
6
+ Project-URL: Issues, https://github.com/Bhavye2003Developer/memlint/issues
7
+ Author-email: Bhavye <bhavyedevelopment2003@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Requires-Python: >=3.11
11
+ Requires-Dist: click>=8.0
12
+ Requires-Dist: pydantic>=2.0
13
+ Requires-Dist: python-dateutil>=2.8
14
+ Requires-Dist: python-dotenv>=1.0
15
+ Requires-Dist: rich>=13.0
16
+ Provides-Extra: dev
17
+ Requires-Dist: pytest-cov; extra == 'dev'
18
+ Requires-Dist: pytest>=8.0; extra == 'dev'
19
+ Provides-Extra: llm
20
+ Requires-Dist: langchain-core>=0.2; extra == 'llm'
21
+ Requires-Dist: langchain-openai>=0.1; extra == 'llm'
22
+ Description-Content-Type: text/markdown
23
+
24
+ # memlint
25
+
26
+ **Lint your LLM agent's memory before it lies to you.**
27
+
28
+ `memlint` detects stale facts in an LLM agent's memory store before they are injected into the context window. It scores each fact by age, confirmation history, and contradiction signals, then tells you which ones to flag, refresh, or discard.
29
+
30
+ ## The problem
31
+
32
+ LLM agents that work across sessions store facts about the user and world - where they live, where they work, what they're building. These facts go stale when the real world changes but the memory doesn't. A fact like `"User works at xyz"` stays in memory after a job change. The agent retrieves it, injects it, and answers confidently with wrong information.
33
+
34
+ `memlint` catches this before it happens.
35
+
36
+ ## Installation
37
+
38
+ ```bash
39
+ pip install memlint
40
+ ```
41
+
42
+ With optional LLM-assisted classification:
43
+
44
+ ```bash
45
+ pip install memlint[llm]
46
+ ```
47
+
48
+ ## Quick Start
49
+
50
+ ```python
51
+ from memlint import StaleDetector
52
+ from memlint.adapters.json_adapter import load_from_json
53
+
54
+ facts = load_from_json("sample_memories.json")
55
+ detector = StaleDetector()
56
+ report = detector.check(facts)
57
+
58
+ print(f"Total: {report.total_facts} | Flagged: {len(report.flagged)}")
59
+ for result in report.flagged:
60
+ print(f" [{result.staleness_level.value.upper()}] {result.content}")
61
+ print(f" Reason: {result.reason}")
62
+ print(f" Action: {result.recommendation}")
63
+ ```
64
+
65
+ ## CLI Usage
66
+
67
+ Check all facts:
68
+ ```bash
69
+ memlint check memories.json
70
+ ```
71
+
72
+ Show only stale and expired:
73
+ ```bash
74
+ memlint check memories.json --only-flagged
75
+ ```
76
+
77
+ Output raw JSON:
78
+ ```bash
79
+ memlint check memories.json --json
80
+ ```
81
+
82
+ Parse Mem0 format:
83
+ ```bash
84
+ memlint check memories.json --format mem0
85
+ ```
86
+
87
+ Sample output:
88
+ ```
89
+ ╭──────────┬────────────────────────────────────────┬────────────┬─────┬───────┬─────────┬─────────╮
90
+ │ ID │ Content │ Category │ Age │ Score │ Level │ Action │
91
+ ├──────────┼────────────────────────────────────────┼────────────┼─────┼───────┼─────────┼─────────┤
92
+ │ mem_004 │ User works at XYZ as a senior cons... │ employment │ 279 │ 0.70 │ STALE │ flag │
93
+ │ mem_006 │ User debugged a LangGraph memory is... │ episodic │ 29 │ 1.00 │ EXPIRED │ discard │
94
+ ╰──────────┴────────────────────────────────────────┴────────────┴─────┴───────┴─────────┴─────────╯
95
+
96
+ Checked 8 facts: 1 fresh, 2 aging, 3 stale, 2 expired
97
+ ```
98
+
99
+ ## Staleness Score Explained
100
+
101
+ Each fact is assigned a category with a natural lifespan:
102
+
103
+ | Category | Examples | Typical Valid Window |
104
+ |--------------|---------------------------------------|----------------------|
105
+ | `location` | "lives in Delhi", "office in Sector 5"| 6–24 months |
106
+ | `employment` | "works at xyz", "role is consultant" | 6–18 months |
107
+ | `project` | "building pract-agents", "using Pinecone" | 1–6 months |
108
+ | `preference` | "prefers Python", "uses dark mode" | 3–12 months |
109
+ | `relationship`| "manager is X", "team has 5 people" | 3–12 months |
110
+ | `identity` | "name is X", "speaks Hindi" | Very long/permanent |
111
+ | `episodic` | "debugged a LangGraph issue today" | Days to weeks |
112
+ | `system_fact`| "Python version is 3.10", "npm v9" | 1–3 months |
113
+
114
+ Score thresholds:
115
+ - `0.0 – 0.29` → **FRESH** (safe to use)
116
+ - `0.30 – 0.59` → **AGING** (use with caution)
117
+ - `0.60 – 0.79` → **STALE** (flag before injecting)
118
+ - `0.80 – 1.0` → **EXPIRED** (do not inject without reconfirmation)
119
+
120
+ ## Adapters
121
+
122
+ **JSON**: default format:
123
+ ```python
124
+ from memlint.adapters.json_adapter import load_from_json
125
+ facts = load_from_json("memories.json")
126
+ ```
127
+
128
+ **Mem0**: maps `memory` to `content`, `updated_at` to `last_confirmed_at`:
129
+ ```python
130
+ from memlint.adapters.mem0_adapter import load_from_mem0
131
+ facts = load_from_mem0("mem0_export.json")
132
+ ```
133
+
134
+ **LangChain**: two tools: `check_memory_staleness` and `filter_stale_memories` (see below).
135
+
136
+ ## LangChain / LangGraph Integration
137
+
138
+ ```python
139
+ from memlint.adapters.langchain_tool import (
140
+ check_memory_staleness,
141
+ filter_stale_memories,
142
+ )
143
+
144
+ # In a LangGraph node: filter before injecting memories into the LLM
145
+ safe_facts_json = filter_stale_memories.invoke({"facts_json": memories_json_string})
146
+ ```
147
+
148
+ Requires `pip install memlint[llm]`.
149
+
150
+ ## Contributing
151
+
152
+ Open an issue or pull request at the project repository.
@@ -0,0 +1,129 @@
1
+ # memlint
2
+
3
+ **Lint your LLM agent's memory before it lies to you.**
4
+
5
+ `memlint` detects stale facts in an LLM agent's memory store before they are injected into the context window. It scores each fact by age, confirmation history, and contradiction signals, then tells you which ones to flag, refresh, or discard.
6
+
7
+ ## The problem
8
+
9
+ LLM agents that work across sessions store facts about the user and world - where they live, where they work, what they're building. These facts go stale when the real world changes but the memory doesn't. A fact like `"User works at xyz"` stays in memory after a job change. The agent retrieves it, injects it, and answers confidently with wrong information.
10
+
11
+ `memlint` catches this before it happens.
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ pip install memlint
17
+ ```
18
+
19
+ With optional LLM-assisted classification:
20
+
21
+ ```bash
22
+ pip install memlint[llm]
23
+ ```
24
+
25
+ ## Quick Start
26
+
27
+ ```python
28
+ from memlint import StaleDetector
29
+ from memlint.adapters.json_adapter import load_from_json
30
+
31
+ facts = load_from_json("sample_memories.json")
32
+ detector = StaleDetector()
33
+ report = detector.check(facts)
34
+
35
+ print(f"Total: {report.total_facts} | Flagged: {len(report.flagged)}")
36
+ for result in report.flagged:
37
+ print(f" [{result.staleness_level.value.upper()}] {result.content}")
38
+ print(f" Reason: {result.reason}")
39
+ print(f" Action: {result.recommendation}")
40
+ ```
41
+
42
+ ## CLI Usage
43
+
44
+ Check all facts:
45
+ ```bash
46
+ memlint check memories.json
47
+ ```
48
+
49
+ Show only stale and expired:
50
+ ```bash
51
+ memlint check memories.json --only-flagged
52
+ ```
53
+
54
+ Output raw JSON:
55
+ ```bash
56
+ memlint check memories.json --json
57
+ ```
58
+
59
+ Parse Mem0 format:
60
+ ```bash
61
+ memlint check memories.json --format mem0
62
+ ```
63
+
64
+ Sample output:
65
+ ```
66
+ ╭──────────┬────────────────────────────────────────┬────────────┬─────┬───────┬─────────┬─────────╮
67
+ │ ID │ Content │ Category │ Age │ Score │ Level │ Action │
68
+ ├──────────┼────────────────────────────────────────┼────────────┼─────┼───────┼─────────┼─────────┤
69
+ │ mem_004 │ User works at XYZ as a senior cons... │ employment │ 279 │ 0.70 │ STALE │ flag │
70
+ │ mem_006 │ User debugged a LangGraph memory is... │ episodic │ 29 │ 1.00 │ EXPIRED │ discard │
71
+ ╰──────────┴────────────────────────────────────────┴────────────┴─────┴───────┴─────────┴─────────╯
72
+
73
+ Checked 8 facts: 1 fresh, 2 aging, 3 stale, 2 expired
74
+ ```
75
+
76
+ ## Staleness Score Explained
77
+
78
+ Each fact is assigned a category with a natural lifespan:
79
+
80
+ | Category | Examples | Typical Valid Window |
81
+ |--------------|---------------------------------------|----------------------|
82
+ | `location` | "lives in Delhi", "office in Sector 5"| 6–24 months |
83
+ | `employment` | "works at xyz", "role is consultant" | 6–18 months |
84
+ | `project` | "building pract-agents", "using Pinecone" | 1–6 months |
85
+ | `preference` | "prefers Python", "uses dark mode" | 3–12 months |
86
+ | `relationship`| "manager is X", "team has 5 people" | 3–12 months |
87
+ | `identity` | "name is X", "speaks Hindi" | Very long/permanent |
88
+ | `episodic` | "debugged a LangGraph issue today" | Days to weeks |
89
+ | `system_fact`| "Python version is 3.10", "npm v9" | 1–3 months |
90
+
91
+ Score thresholds:
92
+ - `0.0 – 0.29` → **FRESH** (safe to use)
93
+ - `0.30 – 0.59` → **AGING** (use with caution)
94
+ - `0.60 – 0.79` → **STALE** (flag before injecting)
95
+ - `0.80 – 1.0` → **EXPIRED** (do not inject without reconfirmation)
96
+
97
+ ## Adapters
98
+
99
+ **JSON**: default format:
100
+ ```python
101
+ from memlint.adapters.json_adapter import load_from_json
102
+ facts = load_from_json("memories.json")
103
+ ```
104
+
105
+ **Mem0**: maps `memory` to `content`, `updated_at` to `last_confirmed_at`:
106
+ ```python
107
+ from memlint.adapters.mem0_adapter import load_from_mem0
108
+ facts = load_from_mem0("mem0_export.json")
109
+ ```
110
+
111
+ **LangChain**: two tools: `check_memory_staleness` and `filter_stale_memories` (see below).
112
+
113
+ ## LangChain / LangGraph Integration
114
+
115
+ ```python
116
+ from memlint.adapters.langchain_tool import (
117
+ check_memory_staleness,
118
+ filter_stale_memories,
119
+ )
120
+
121
+ # In a LangGraph node: filter before injecting memories into the LLM
122
+ safe_facts_json = filter_stale_memories.invoke({"facts_json": memories_json_string})
123
+ ```
124
+
125
+ Requires `pip install memlint[llm]`.
126
+
127
+ ## Contributing
128
+
129
+ Open an issue or pull request at the project repository.
@@ -0,0 +1,17 @@
1
+ import os
2
+ import sys
3
+
4
+ sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
5
+
6
+ from memlint import StaleDetector
7
+ from memlint.adapters.json_adapter import load_from_json
8
+
9
+ facts = load_from_json(os.path.join(os.path.dirname(__file__), "sample_memories.json"))
10
+ detector = StaleDetector()
11
+ report = detector.check(facts)
12
+
13
+ print(f"Total: {report.total_facts} | Flagged: {len(report.flagged)}")
14
+ for result in report.flagged:
15
+ print(f" [{result.staleness_level.value.upper()}] {result.content}")
16
+ print(f" Reason: {result.reason}")
17
+ print(f" Action: {result.recommendation}")
@@ -0,0 +1,41 @@
1
+ """
2
+ LangChain/LangGraph integration example.
3
+
4
+ Replace the mock invocation with a real LangGraph node in production.
5
+ Requires: pip install memlint[llm]
6
+ """
7
+ import json
8
+ import os
9
+ import sys
10
+
11
+ sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
12
+
13
+ try:
14
+ from memlint.adapters.langchain_tool import (
15
+ check_memory_staleness,
16
+ filter_stale_memories,
17
+ LANGCHAIN_AVAILABLE,
18
+ )
19
+ except ImportError:
20
+ LANGCHAIN_AVAILABLE = False
21
+
22
+ if not LANGCHAIN_AVAILABLE:
23
+ print("langchain-core not installed. Run: pip install memlint[llm]")
24
+ sys.exit(0)
25
+
26
+ # --- Replace with real LangGraph node invocation in production ---
27
+ sample_fact = {
28
+ "id": "mem_001",
29
+ "content": "User works at PwC",
30
+ "created_at": "2024-09-01T00:00:00",
31
+ "confirmation_count": 0,
32
+ "source": "user",
33
+ }
34
+
35
+ # Tool 1: check a single fact
36
+ result_json = check_memory_staleness.invoke({"fact_json": json.dumps(sample_fact)})
37
+ print("Single fact result:", result_json)
38
+
39
+ # Tool 2: filter a list — returns only FRESH and AGING facts
40
+ safe_json = filter_stale_memories.invoke({"facts_json": json.dumps([sample_fact])})
41
+ print("Safe facts:", safe_json)
@@ -0,0 +1,59 @@
1
+ [
2
+ {
3
+ "id": "mem_001",
4
+ "content": "User prefers Python over JavaScript",
5
+ "created_at": "2026-05-20T09:00:00",
6
+ "last_confirmed_at": "2026-06-01T09:00:00",
7
+ "confirmation_count": 3,
8
+ "source": "user"
9
+ },
10
+ {
11
+ "id": "mem_002",
12
+ "content": "User prefers dark mode in all editors",
13
+ "created_at": "2026-01-28T10:00:00",
14
+ "confirmation_count": 0,
15
+ "source": "user"
16
+ },
17
+ {
18
+ "id": "mem_003",
19
+ "content": "User is based in New Delhi for work",
20
+ "created_at": "2025-11-19T08:00:00",
21
+ "confirmation_count": 0,
22
+ "source": "user"
23
+ },
24
+ {
25
+ "id": "mem_004",
26
+ "content": "User works at PwC as a senior consultant",
27
+ "created_at": "2025-08-31T09:00:00",
28
+ "confirmation_count": 0,
29
+ "source": "user"
30
+ },
31
+ {
32
+ "id": "mem_005",
33
+ "content": "User is building pract-agents using LangGraph framework",
34
+ "created_at": "2026-02-07T11:00:00",
35
+ "confirmation_count": 0,
36
+ "source": "user"
37
+ },
38
+ {
39
+ "id": "mem_006",
40
+ "content": "User debugged a LangGraph memory issue this morning",
41
+ "created_at": "2026-05-08T07:00:00",
42
+ "confirmation_count": 0,
43
+ "source": "user"
44
+ },
45
+ {
46
+ "id": "mem_007",
47
+ "content": "User lives in Delhi",
48
+ "created_at": "2025-05-03T10:00:00",
49
+ "confirmation_count": 0,
50
+ "source": "user"
51
+ },
52
+ {
53
+ "id": "mem_008",
54
+ "content": "User lives in Mumbai now",
55
+ "created_at": "2026-04-18T10:00:00",
56
+ "confirmation_count": 0,
57
+ "source": "user"
58
+ }
59
+ ]
@@ -0,0 +1,19 @@
1
+ from memlint.core import StaleDetector
2
+ from memlint.models import (
3
+ MemoryFact,
4
+ StalenessResult,
5
+ DetectionReport,
6
+ FactCategory,
7
+ StalenessLevel,
8
+ )
9
+ from memlint.classifier import classify_fact_async
10
+
11
+ __all__ = [
12
+ "StaleDetector",
13
+ "MemoryFact",
14
+ "StalenessResult",
15
+ "DetectionReport",
16
+ "FactCategory",
17
+ "StalenessLevel",
18
+ "classify_fact_async",
19
+ ]
File without changes
@@ -0,0 +1,8 @@
1
+ from datetime import datetime
2
+ from dateutil import parser as dateutil_parser
3
+
4
+
5
+ def parse_dt(value: str | None) -> datetime | None:
6
+ if value is None:
7
+ return None
8
+ return dateutil_parser.parse(value).replace(tzinfo=None)
@@ -0,0 +1,28 @@
1
+ import json
2
+ from memlint.adapters._utils import parse_dt
3
+ from memlint.models import MemoryFact
4
+
5
+
6
+ def load_from_json(filepath: str) -> list[MemoryFact]:
7
+ with open(filepath, "r", encoding="utf-8") as f:
8
+ data = json.load(f)
9
+
10
+ if not isinstance(data, list):
11
+ raise ValueError(f"Expected a JSON array at root, got {type(data).__name__}")
12
+
13
+ facts = []
14
+ for i, entry in enumerate(data):
15
+ for required in ("id", "content", "created_at"):
16
+ if required not in entry:
17
+ raise ValueError(f"Entry {i} missing required field '{required}'")
18
+
19
+ facts.append(MemoryFact(
20
+ id=entry["id"],
21
+ content=entry["content"],
22
+ created_at=parse_dt(entry["created_at"]),
23
+ last_confirmed_at=parse_dt(entry.get("last_confirmed_at")),
24
+ confirmation_count=entry.get("confirmation_count", 0),
25
+ source=entry.get("source", "user"),
26
+ metadata=entry.get("metadata", {}),
27
+ ))
28
+ return facts
@@ -0,0 +1,25 @@
1
+ import json
2
+ from memlint.models import MemoryFact
3
+
4
+ try:
5
+ from langchain_core.tools import tool
6
+ LANGCHAIN_AVAILABLE = True
7
+ except ImportError:
8
+ LANGCHAIN_AVAILABLE = False
9
+
10
+ if LANGCHAIN_AVAILABLE:
11
+ from memlint.core import StaleDetector
12
+
13
+ @tool
14
+ def check_memory_staleness(fact_json: str) -> str:
15
+ """Check if a single memory fact is stale before injecting it into context."""
16
+ fact = MemoryFact.model_validate(json.loads(fact_json))
17
+ result = StaleDetector().check_one(fact)
18
+ return result.model_dump_json()
19
+
20
+ @tool
21
+ def filter_stale_memories(facts_json: str) -> str:
22
+ """Filter out stale and expired memory facts from a list, returning only safe-to-use facts."""
23
+ facts = [MemoryFact.model_validate(d) for d in json.loads(facts_json)]
24
+ safe = StaleDetector().filter_safe(facts)
25
+ return json.dumps([f.model_dump() for f in safe], default=str)
@@ -0,0 +1,28 @@
1
+ import json
2
+ from memlint.adapters._utils import parse_dt
3
+ from memlint.models import MemoryFact
4
+
5
+
6
+ def load_from_mem0(filepath: str) -> list[MemoryFact]:
7
+ with open(filepath, "r", encoding="utf-8") as f:
8
+ data = json.load(f)
9
+
10
+ if not isinstance(data, list):
11
+ raise ValueError(f"Expected a JSON array at root, got {type(data).__name__}")
12
+
13
+ facts = []
14
+ for i, entry in enumerate(data):
15
+ for required in ("id", "memory", "created_at"):
16
+ if required not in entry:
17
+ raise ValueError(f"Entry {i} missing required field '{required}'")
18
+
19
+ facts.append(MemoryFact(
20
+ id=entry["id"],
21
+ content=entry["memory"],
22
+ created_at=parse_dt(entry["created_at"]),
23
+ last_confirmed_at=parse_dt(entry.get("updated_at")),
24
+ confirmation_count=entry.get("confirmation_count", 0),
25
+ source=entry.get("source", "user"),
26
+ metadata=entry.get("metadata", {}),
27
+ ))
28
+ return facts