dreamguard 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.
- dreamguard-0.1.0/LICENSE +21 -0
- dreamguard-0.1.0/MANIFEST.in +2 -0
- dreamguard-0.1.0/PKG-INFO +201 -0
- dreamguard-0.1.0/README.md +167 -0
- dreamguard-0.1.0/dreamguard/__init__.py +6 -0
- dreamguard-0.1.0/dreamguard/__main__.py +3 -0
- dreamguard-0.1.0/dreamguard/api.py +141 -0
- dreamguard-0.1.0/dreamguard/benchmark.py +76 -0
- dreamguard-0.1.0/dreamguard/cli.py +34 -0
- dreamguard-0.1.0/dreamguard/compiler.py +79 -0
- dreamguard-0.1.0/dreamguard/db.py +30 -0
- dreamguard-0.1.0/dreamguard/engine.py +91 -0
- dreamguard-0.1.0/dreamguard/live.py +112 -0
- dreamguard-0.1.0/dreamguard/py.typed +0 -0
- dreamguard-0.1.0/dreamguard/static/app.js +83 -0
- dreamguard-0.1.0/dreamguard/static/index.html +49 -0
- dreamguard-0.1.0/dreamguard/static/live.html +41 -0
- dreamguard-0.1.0/dreamguard/static/live.js +65 -0
- dreamguard-0.1.0/dreamguard/static/style.css +38 -0
- dreamguard-0.1.0/dreamguard.egg-info/PKG-INFO +201 -0
- dreamguard-0.1.0/dreamguard.egg-info/SOURCES.txt +26 -0
- dreamguard-0.1.0/dreamguard.egg-info/dependency_links.txt +1 -0
- dreamguard-0.1.0/dreamguard.egg-info/entry_points.txt +2 -0
- dreamguard-0.1.0/dreamguard.egg-info/requires.txt +15 -0
- dreamguard-0.1.0/dreamguard.egg-info/top_level.txt +1 -0
- dreamguard-0.1.0/pyproject.toml +45 -0
- dreamguard-0.1.0/setup.cfg +4 -0
- dreamguard-0.1.0/tests/test_engine.py +87 -0
dreamguard-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Manojkumar V
|
|
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.
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dreamguard
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Reversible memory compilation for AI agents: compress memory without losing the raw data, and roll back when recall drops.
|
|
5
|
+
Author: Manojkumar V
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/manojkumar16122/dreamguard
|
|
8
|
+
Project-URL: Issues, https://github.com/manojkumar16122/dreamguard/issues
|
|
9
|
+
Keywords: ai,agents,llm,memory,rollback,context-compression
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Provides-Extra: groq
|
|
22
|
+
Requires-Dist: groq>=0.11; extra == "groq"
|
|
23
|
+
Provides-Extra: server
|
|
24
|
+
Requires-Dist: fastapi>=0.110; extra == "server"
|
|
25
|
+
Requires-Dist: uvicorn>=0.29; extra == "server"
|
|
26
|
+
Requires-Dist: python-dotenv>=1.0; extra == "server"
|
|
27
|
+
Requires-Dist: groq>=0.11; extra == "server"
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
30
|
+
Requires-Dist: httpx>=0.27; extra == "dev"
|
|
31
|
+
Requires-Dist: build; extra == "dev"
|
|
32
|
+
Requires-Dist: twine; extra == "dev"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# 🛡️ DreamGuard
|
|
36
|
+
|
|
37
|
+
**Reversible memory compilation for AI agents.**
|
|
38
|
+
|
|
39
|
+
[](#) [](LICENSE) [](#testing)
|
|
40
|
+
|
|
41
|
+
Long-running agents need to compress their memory ("dreaming" / consolidation) or the context window bloats. The common approach rewrites memory **in place**: once a detail is summarised away, it is gone for good.
|
|
42
|
+
|
|
43
|
+
DreamGuard compiles raw *episodic* memory into compact *semantic* memory like a build step: **the raw episodes are never deleted**, and every compressed fact is linked back to its source episodes. If recall quality drops, you **roll back** and the exact raw context is restored.
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
record() ──► episodes (immutable)
|
|
47
|
+
│ dream()
|
|
48
|
+
▼
|
|
49
|
+
semantic_memories ──provenance──► episodes
|
|
50
|
+
│ rollback()
|
|
51
|
+
▼
|
|
52
|
+
compressed facts deactivated, raw context restored
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Demo
|
|
56
|
+
|
|
57
|
+
Run the dashboard and open <http://localhost:8000>. It plots detail-recall accuracy over 50 turns for three systems and lets you trigger a rollback:
|
|
58
|
+
|
|
59
|
+
| System | What it does | Behaviour in the demo |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| Full raw context (reference) | Never compresses | 100%, but context grows without bound |
|
|
62
|
+
| In-place dreaming | Rewrites memory, deletes raw data | Loss compounds, cannot recover |
|
|
63
|
+
| **DreamGuard** | Compresses, keeps raw + provenance | Same loss as in-place, then **recovers to 100%** on rollback |
|
|
64
|
+
|
|
65
|
+
> 📸 Add a screenshot or GIF here: `docs/demo.gif`
|
|
66
|
+
|
|
67
|
+
## Install
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pip install dreamguard # core library, zero dependencies
|
|
71
|
+
pip install "dreamguard[server]" # + dashboard, API and Groq support
|
|
72
|
+
pip install "dreamguard[groq]" # + Groq compressor only
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Quick start
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pip install "dreamguard[server]"
|
|
79
|
+
echo "GROQ_API_KEY=gsk_..." > .env # only needed for the /live demo
|
|
80
|
+
dreamguard serve # open http://localhost:8000
|
|
81
|
+
dreamguard benchmark # offline simulation in the terminal
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Development setup: `git clone ... && pip install -e ".[dev,server]" && pytest`.
|
|
85
|
+
|
|
86
|
+
### Use it as a library
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from dreamguard import MemoryEngine, HeuristicCompressor
|
|
90
|
+
|
|
91
|
+
mem = MemoryEngine(HeuristicCompressor(retention=0.7), destructive=False)
|
|
92
|
+
mem.record(1, "api_port=8421")
|
|
93
|
+
mem.record(2, "region=ap-south-1")
|
|
94
|
+
|
|
95
|
+
mem.dream() # compress; raw episodes + provenance are kept
|
|
96
|
+
print(mem.context()) # what your agent would see (digest)
|
|
97
|
+
mem.rollback() # back to exact raw episodes
|
|
98
|
+
print(mem.context())
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Live demo with a real LLM (Groq)
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
# .env
|
|
105
|
+
GROQ_API_KEY=gsk_...
|
|
106
|
+
```
|
|
107
|
+
Run `uvicorn dreamguard.api:app --reload`, open <http://localhost:8000/live>, then click:
|
|
108
|
+
**1 Load scenario → 2 Evaluate → 3 Dream → 2 Evaluate → 4 Rollback → 2 Evaluate.**
|
|
109
|
+
|
|
110
|
+
Groq compresses 12 engineering-log episodes into a short digest, then answers 8 factual questions (ports, ticket owners, hosts...) using *only* each system's remaining memory. Answers are graded against known ground truth. Both the in-place and DreamGuard engines get the same input; only DreamGuard can roll back. Set `GROQ_MODEL` to change the model (default `llama-3.1-8b-instant`).
|
|
111
|
+
|
|
112
|
+
Results are real LLM output, so they vary by model and run: a strong model may retain some details after compression. The rollback guarantee does not vary, because it restores the raw text.
|
|
113
|
+
|
|
114
|
+
### Real LLM compression (optional)
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
# .env
|
|
118
|
+
COMPRESSOR=groq
|
|
119
|
+
GROQ_API_KEY=gsk_...
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Uses Groq's `llama-3.1-8b-instant` to produce the digest. Note: the built-in **benchmark** always uses the heuristic compressor so that its results are reproducible.
|
|
123
|
+
|
|
124
|
+
## How it works
|
|
125
|
+
|
|
126
|
+
| Table | Purpose |
|
|
127
|
+
|---|---|
|
|
128
|
+
| `episodes` | Raw, append-only memory (`turn`, `content`) |
|
|
129
|
+
| `semantic_memories` | Compressed digests, with an `active` flag |
|
|
130
|
+
| `provenance` | `memory_id → episode_id` links from each digest to its sources |
|
|
131
|
+
|
|
132
|
+
- `record(turn, content)` appends an episode.
|
|
133
|
+
- `dream()` compresses episodes not yet covered by an active digest.
|
|
134
|
+
- `rollback()` sets all digests to inactive. Their episodes become "uncovered" again, so `context()` returns the raw text.
|
|
135
|
+
- `context()` is what an agent reads: active digests plus any uncovered raw episodes.
|
|
136
|
+
- With `destructive=True` the engine mimics in-place dreaming: it deletes raw data and re-compresses digests together with new episodes, so loss compounds.
|
|
137
|
+
|
|
138
|
+
## Benchmark methodology (read this before quoting numbers)
|
|
139
|
+
|
|
140
|
+
The benchmark is a **synthetic simulation**, not a measurement of any production system:
|
|
141
|
+
|
|
142
|
+
1. Each turn records one exact detail, e.g. `param_7=48213`.
|
|
143
|
+
2. Every 10 turns the memory is compressed with a seeded lossy compressor that keeps each value with probability 0.7 and otherwise keeps only the key.
|
|
144
|
+
3. After every turn, **recall** = fraction of all details seen so far that appear verbatim (`key=value`) in the agent's visible context.
|
|
145
|
+
|
|
146
|
+
What this shows: *if* compression is lossy, an irreversible design loses information permanently, while a design that keeps raw data can restore it. What it does **not** show: how any specific LLM, product, or published system behaves. The absolute accuracy numbers come from the chosen retention parameter, not from real-world measurement.
|
|
147
|
+
|
|
148
|
+
**Known trade-off:** after a rollback, the agent context is raw again and therefore larger. The benefit of compression (smaller context) is traded for recall until the next dream.
|
|
149
|
+
|
|
150
|
+
## API
|
|
151
|
+
|
|
152
|
+
| Method | Path | Description |
|
|
153
|
+
|---|---|---|
|
|
154
|
+
| POST | `/api/record` | `{"content": "..."}` record an episode |
|
|
155
|
+
| POST | `/api/dream` | Compress uncovered episodes |
|
|
156
|
+
| POST | `/api/rollback` | Deactivate compressed memory |
|
|
157
|
+
| POST | `/api/reset` | Clear the live engine |
|
|
158
|
+
| POST | `/api/demo/seed?n=10` | Add demo episodes |
|
|
159
|
+
| GET | `/api/memory` | Episodes, facts, provenance, context size |
|
|
160
|
+
| GET | `/api/benchmark` | Run benchmark (`seed`, `retention`, `auto_rollback`, `rollback_at`) |
|
|
161
|
+
|
|
162
|
+
Interactive docs at `/docs`.
|
|
163
|
+
|
|
164
|
+
## Project layout
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
dreamguard/
|
|
168
|
+
├── dreamguard/
|
|
169
|
+
│ ├── db.py # SQLite schema
|
|
170
|
+
│ ├── compiler.py # Heuristic + Groq compressors
|
|
171
|
+
│ ├── engine.py # record / dream / rollback / evaluate
|
|
172
|
+
│ ├── benchmark.py # reproducible simulation
|
|
173
|
+
│ ├── live.py # live demo: Groq compression + Groq QA grading
|
|
174
|
+
│ ├── api.py # FastAPI app
|
|
175
|
+
│ ├── cli.py # `dreamguard` command
|
|
176
|
+
│ └── static/ # dashboard (HTML + CSS + Chart.js, no build step)
|
|
177
|
+
├── tests/
|
|
178
|
+
└── pyproject.toml
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Testing
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
pip install -e ".[dev,server]"
|
|
185
|
+
pytest
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Roadmap
|
|
189
|
+
|
|
190
|
+
- [ ] Selective rollback (restore only the episodes behind a specific fact)
|
|
191
|
+
- [ ] Benchmark with real LLM compression and an LLM-answered QA task
|
|
192
|
+
- [ ] Pluggable storage backends (Postgres, vector DBs)
|
|
193
|
+
- [ ] Rollback triggers driven by task-level eval signals
|
|
194
|
+
|
|
195
|
+
## Contributing
|
|
196
|
+
|
|
197
|
+
Issues and PRs are welcome. Please run `pytest` before submitting.
|
|
198
|
+
|
|
199
|
+
## License
|
|
200
|
+
|
|
201
|
+
MIT, see [LICENSE](LICENSE). Built by [Manojkumar V](https://github.com/manojkumar16122).
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# 🛡️ DreamGuard
|
|
2
|
+
|
|
3
|
+
**Reversible memory compilation for AI agents.**
|
|
4
|
+
|
|
5
|
+
[](#) [](LICENSE) [](#testing)
|
|
6
|
+
|
|
7
|
+
Long-running agents need to compress their memory ("dreaming" / consolidation) or the context window bloats. The common approach rewrites memory **in place**: once a detail is summarised away, it is gone for good.
|
|
8
|
+
|
|
9
|
+
DreamGuard compiles raw *episodic* memory into compact *semantic* memory like a build step: **the raw episodes are never deleted**, and every compressed fact is linked back to its source episodes. If recall quality drops, you **roll back** and the exact raw context is restored.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
record() ──► episodes (immutable)
|
|
13
|
+
│ dream()
|
|
14
|
+
▼
|
|
15
|
+
semantic_memories ──provenance──► episodes
|
|
16
|
+
│ rollback()
|
|
17
|
+
▼
|
|
18
|
+
compressed facts deactivated, raw context restored
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Demo
|
|
22
|
+
|
|
23
|
+
Run the dashboard and open <http://localhost:8000>. It plots detail-recall accuracy over 50 turns for three systems and lets you trigger a rollback:
|
|
24
|
+
|
|
25
|
+
| System | What it does | Behaviour in the demo |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| Full raw context (reference) | Never compresses | 100%, but context grows without bound |
|
|
28
|
+
| In-place dreaming | Rewrites memory, deletes raw data | Loss compounds, cannot recover |
|
|
29
|
+
| **DreamGuard** | Compresses, keeps raw + provenance | Same loss as in-place, then **recovers to 100%** on rollback |
|
|
30
|
+
|
|
31
|
+
> 📸 Add a screenshot or GIF here: `docs/demo.gif`
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install dreamguard # core library, zero dependencies
|
|
37
|
+
pip install "dreamguard[server]" # + dashboard, API and Groq support
|
|
38
|
+
pip install "dreamguard[groq]" # + Groq compressor only
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quick start
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install "dreamguard[server]"
|
|
45
|
+
echo "GROQ_API_KEY=gsk_..." > .env # only needed for the /live demo
|
|
46
|
+
dreamguard serve # open http://localhost:8000
|
|
47
|
+
dreamguard benchmark # offline simulation in the terminal
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Development setup: `git clone ... && pip install -e ".[dev,server]" && pytest`.
|
|
51
|
+
|
|
52
|
+
### Use it as a library
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from dreamguard import MemoryEngine, HeuristicCompressor
|
|
56
|
+
|
|
57
|
+
mem = MemoryEngine(HeuristicCompressor(retention=0.7), destructive=False)
|
|
58
|
+
mem.record(1, "api_port=8421")
|
|
59
|
+
mem.record(2, "region=ap-south-1")
|
|
60
|
+
|
|
61
|
+
mem.dream() # compress; raw episodes + provenance are kept
|
|
62
|
+
print(mem.context()) # what your agent would see (digest)
|
|
63
|
+
mem.rollback() # back to exact raw episodes
|
|
64
|
+
print(mem.context())
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Live demo with a real LLM (Groq)
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# .env
|
|
71
|
+
GROQ_API_KEY=gsk_...
|
|
72
|
+
```
|
|
73
|
+
Run `uvicorn dreamguard.api:app --reload`, open <http://localhost:8000/live>, then click:
|
|
74
|
+
**1 Load scenario → 2 Evaluate → 3 Dream → 2 Evaluate → 4 Rollback → 2 Evaluate.**
|
|
75
|
+
|
|
76
|
+
Groq compresses 12 engineering-log episodes into a short digest, then answers 8 factual questions (ports, ticket owners, hosts...) using *only* each system's remaining memory. Answers are graded against known ground truth. Both the in-place and DreamGuard engines get the same input; only DreamGuard can roll back. Set `GROQ_MODEL` to change the model (default `llama-3.1-8b-instant`).
|
|
77
|
+
|
|
78
|
+
Results are real LLM output, so they vary by model and run: a strong model may retain some details after compression. The rollback guarantee does not vary, because it restores the raw text.
|
|
79
|
+
|
|
80
|
+
### Real LLM compression (optional)
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# .env
|
|
84
|
+
COMPRESSOR=groq
|
|
85
|
+
GROQ_API_KEY=gsk_...
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Uses Groq's `llama-3.1-8b-instant` to produce the digest. Note: the built-in **benchmark** always uses the heuristic compressor so that its results are reproducible.
|
|
89
|
+
|
|
90
|
+
## How it works
|
|
91
|
+
|
|
92
|
+
| Table | Purpose |
|
|
93
|
+
|---|---|
|
|
94
|
+
| `episodes` | Raw, append-only memory (`turn`, `content`) |
|
|
95
|
+
| `semantic_memories` | Compressed digests, with an `active` flag |
|
|
96
|
+
| `provenance` | `memory_id → episode_id` links from each digest to its sources |
|
|
97
|
+
|
|
98
|
+
- `record(turn, content)` appends an episode.
|
|
99
|
+
- `dream()` compresses episodes not yet covered by an active digest.
|
|
100
|
+
- `rollback()` sets all digests to inactive. Their episodes become "uncovered" again, so `context()` returns the raw text.
|
|
101
|
+
- `context()` is what an agent reads: active digests plus any uncovered raw episodes.
|
|
102
|
+
- With `destructive=True` the engine mimics in-place dreaming: it deletes raw data and re-compresses digests together with new episodes, so loss compounds.
|
|
103
|
+
|
|
104
|
+
## Benchmark methodology (read this before quoting numbers)
|
|
105
|
+
|
|
106
|
+
The benchmark is a **synthetic simulation**, not a measurement of any production system:
|
|
107
|
+
|
|
108
|
+
1. Each turn records one exact detail, e.g. `param_7=48213`.
|
|
109
|
+
2. Every 10 turns the memory is compressed with a seeded lossy compressor that keeps each value with probability 0.7 and otherwise keeps only the key.
|
|
110
|
+
3. After every turn, **recall** = fraction of all details seen so far that appear verbatim (`key=value`) in the agent's visible context.
|
|
111
|
+
|
|
112
|
+
What this shows: *if* compression is lossy, an irreversible design loses information permanently, while a design that keeps raw data can restore it. What it does **not** show: how any specific LLM, product, or published system behaves. The absolute accuracy numbers come from the chosen retention parameter, not from real-world measurement.
|
|
113
|
+
|
|
114
|
+
**Known trade-off:** after a rollback, the agent context is raw again and therefore larger. The benefit of compression (smaller context) is traded for recall until the next dream.
|
|
115
|
+
|
|
116
|
+
## API
|
|
117
|
+
|
|
118
|
+
| Method | Path | Description |
|
|
119
|
+
|---|---|---|
|
|
120
|
+
| POST | `/api/record` | `{"content": "..."}` record an episode |
|
|
121
|
+
| POST | `/api/dream` | Compress uncovered episodes |
|
|
122
|
+
| POST | `/api/rollback` | Deactivate compressed memory |
|
|
123
|
+
| POST | `/api/reset` | Clear the live engine |
|
|
124
|
+
| POST | `/api/demo/seed?n=10` | Add demo episodes |
|
|
125
|
+
| GET | `/api/memory` | Episodes, facts, provenance, context size |
|
|
126
|
+
| GET | `/api/benchmark` | Run benchmark (`seed`, `retention`, `auto_rollback`, `rollback_at`) |
|
|
127
|
+
|
|
128
|
+
Interactive docs at `/docs`.
|
|
129
|
+
|
|
130
|
+
## Project layout
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
dreamguard/
|
|
134
|
+
├── dreamguard/
|
|
135
|
+
│ ├── db.py # SQLite schema
|
|
136
|
+
│ ├── compiler.py # Heuristic + Groq compressors
|
|
137
|
+
│ ├── engine.py # record / dream / rollback / evaluate
|
|
138
|
+
│ ├── benchmark.py # reproducible simulation
|
|
139
|
+
│ ├── live.py # live demo: Groq compression + Groq QA grading
|
|
140
|
+
│ ├── api.py # FastAPI app
|
|
141
|
+
│ ├── cli.py # `dreamguard` command
|
|
142
|
+
│ └── static/ # dashboard (HTML + CSS + Chart.js, no build step)
|
|
143
|
+
├── tests/
|
|
144
|
+
└── pyproject.toml
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Testing
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
pip install -e ".[dev,server]"
|
|
151
|
+
pytest
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Roadmap
|
|
155
|
+
|
|
156
|
+
- [ ] Selective rollback (restore only the episodes behind a specific fact)
|
|
157
|
+
- [ ] Benchmark with real LLM compression and an LLM-answered QA task
|
|
158
|
+
- [ ] Pluggable storage backends (Postgres, vector DBs)
|
|
159
|
+
- [ ] Rollback triggers driven by task-level eval signals
|
|
160
|
+
|
|
161
|
+
## Contributing
|
|
162
|
+
|
|
163
|
+
Issues and PRs are welcome. Please run `pytest` before submitting.
|
|
164
|
+
|
|
165
|
+
## License
|
|
166
|
+
|
|
167
|
+
MIT, see [LICENSE](LICENSE). Built by [Manojkumar V](https://github.com/manojkumar16122).
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""DreamGuard: reversible memory compilation for AI agents."""
|
|
2
|
+
from .engine import MemoryEngine
|
|
3
|
+
from .compiler import HeuristicCompressor, GroqCompressor, get_compressor
|
|
4
|
+
|
|
5
|
+
__version__ = "0.1.0"
|
|
6
|
+
__all__ = ["MemoryEngine", "HeuristicCompressor", "GroqCompressor", "get_compressor"]
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""FastAPI backend + static dashboard. Run: uvicorn dreamguard.api:app --reload"""
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from dotenv import load_dotenv
|
|
5
|
+
from fastapi import FastAPI, HTTPException
|
|
6
|
+
from fastapi.responses import FileResponse
|
|
7
|
+
from fastapi.staticfiles import StaticFiles
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
from .benchmark import run_benchmark
|
|
11
|
+
from .compiler import get_compressor
|
|
12
|
+
from .engine import MemoryEngine
|
|
13
|
+
from .live import LiveDemo
|
|
14
|
+
|
|
15
|
+
load_dotenv()
|
|
16
|
+
STATIC = Path(__file__).resolve().parent / "static"
|
|
17
|
+
|
|
18
|
+
app = FastAPI(title="DreamGuard", version="0.1.0")
|
|
19
|
+
app.mount("/static", StaticFiles(directory=STATIC), name="static")
|
|
20
|
+
|
|
21
|
+
_state = {"engine": None, "turn": 0}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _engine() -> MemoryEngine:
|
|
25
|
+
if _state["engine"] is None:
|
|
26
|
+
_state["engine"] = MemoryEngine(get_compressor(), destructive=False)
|
|
27
|
+
return _state["engine"]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Episode(BaseModel):
|
|
31
|
+
content: str = Field(min_length=1, max_length=2000)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@app.get("/")
|
|
35
|
+
def index():
|
|
36
|
+
return FileResponse(STATIC / "index.html")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@app.post("/api/record")
|
|
40
|
+
def record(ep: Episode):
|
|
41
|
+
_state["turn"] += 1
|
|
42
|
+
_engine().record(_state["turn"], ep.content)
|
|
43
|
+
return {"turn": _state["turn"]}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@app.post("/api/dream")
|
|
47
|
+
def dream():
|
|
48
|
+
try:
|
|
49
|
+
fact = _engine().dream()
|
|
50
|
+
except Exception as e: # e.g. missing GROQ key / network error
|
|
51
|
+
raise HTTPException(500, f"Compression failed: {e}")
|
|
52
|
+
return {"fact": fact}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@app.post("/api/rollback")
|
|
56
|
+
def rollback():
|
|
57
|
+
return {"rolled_back": _engine().rollback()}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@app.post("/api/reset")
|
|
61
|
+
def reset():
|
|
62
|
+
_state.update(engine=None, turn=0)
|
|
63
|
+
return {"ok": True}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@app.post("/api/demo/seed")
|
|
67
|
+
def seed(n: int = 10):
|
|
68
|
+
eng = _engine()
|
|
69
|
+
for _ in range(n):
|
|
70
|
+
_state["turn"] += 1
|
|
71
|
+
t = _state["turn"]
|
|
72
|
+
eng.record(t, f"param_{t}={(t * 7919) % 90000 + 10000}")
|
|
73
|
+
return {"turn": _state["turn"]}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@app.get("/api/memory")
|
|
77
|
+
def memory():
|
|
78
|
+
return _engine().snapshot()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@app.get("/api/benchmark")
|
|
82
|
+
def benchmark(seed: int = 42, retention: float = 0.7, auto_rollback: bool = False,
|
|
83
|
+
rollback_at: int | None = 40):
|
|
84
|
+
return run_benchmark(seed=seed, retention=retention, auto_rollback=auto_rollback,
|
|
85
|
+
rollback_at=None if auto_rollback else rollback_at)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ---- live demo (real Groq compression + Groq question answering) ----------
|
|
89
|
+
_live: dict = {"demo": None}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _demo() -> LiveDemo:
|
|
93
|
+
if _live["demo"] is None:
|
|
94
|
+
raise HTTPException(400, "Click 'Load scenario' first.")
|
|
95
|
+
return _live["demo"]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _guard(fn):
|
|
99
|
+
try:
|
|
100
|
+
return fn()
|
|
101
|
+
except HTTPException:
|
|
102
|
+
raise
|
|
103
|
+
except Exception as e: # missing key, rate limit, network, bad JSON...
|
|
104
|
+
raise HTTPException(502, f"Groq error: {e}")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@app.get("/live")
|
|
108
|
+
def live_page():
|
|
109
|
+
return FileResponse(STATIC / "live.html")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@app.post("/api/live/start")
|
|
113
|
+
def live_start():
|
|
114
|
+
_live["demo"] = _guard(LiveDemo)
|
|
115
|
+
return _live["demo"].state()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@app.post("/api/live/dream")
|
|
119
|
+
def live_dream():
|
|
120
|
+
d = _demo()
|
|
121
|
+
_guard(d.dream)
|
|
122
|
+
return d.state()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@app.post("/api/live/rollback")
|
|
126
|
+
def live_rollback():
|
|
127
|
+
d = _demo()
|
|
128
|
+
d.rollback()
|
|
129
|
+
return d.state()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@app.post("/api/live/evaluate")
|
|
133
|
+
def live_evaluate(label: str = "check"):
|
|
134
|
+
d = _demo()
|
|
135
|
+
_guard(lambda: d.evaluate(label[:40]))
|
|
136
|
+
return d.state()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@app.get("/api/live/state")
|
|
140
|
+
def live_state():
|
|
141
|
+
return _demo().state()
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Synthetic detail-retention benchmark.
|
|
2
|
+
|
|
3
|
+
Each turn the agent records an episode containing one exact detail
|
|
4
|
+
(`param_7=48213`). Every `dream_every` turns the memory is compressed. After
|
|
5
|
+
each turn we probe recall of ALL details seen so far (exact match against the
|
|
6
|
+
agent's visible context). No LLM is involved, so results are deterministic.
|
|
7
|
+
|
|
8
|
+
Systems compared:
|
|
9
|
+
raw - never compresses (accuracy reference; context grows unbounded)
|
|
10
|
+
in_place - compresses by rewriting memory, raw data deleted
|
|
11
|
+
dreamguard - same compressor, raw data kept. Rolls back either manually at
|
|
12
|
+
`rollback_at` (default: turn 40, for a visible drop-and-recovery
|
|
13
|
+
demo) or automatically when probe accuracy < threshold
|
|
14
|
+
(`auto_rollback=True`). Dreaming then pauses for `cooldown` turns.
|
|
15
|
+
"""
|
|
16
|
+
import json
|
|
17
|
+
import random
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
from .compiler import HeuristicCompressor
|
|
21
|
+
from .engine import MemoryEngine
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def run_benchmark(turns: int = 50, dream_every: int = 10, retention: float = 0.7,
|
|
25
|
+
threshold: float = 0.85, cooldown: int = 15, seed: int = 42,
|
|
26
|
+
auto_rollback: bool = False, rollback_at: int | None = 40) -> dict:
|
|
27
|
+
rng = random.Random(seed)
|
|
28
|
+
mk = lambda destructive: MemoryEngine(HeuristicCompressor(retention, seed), destructive)
|
|
29
|
+
systems = {"raw": mk(False), "in_place": mk(True), "dreamguard": mk(False)}
|
|
30
|
+
truth: dict[str, str] = {}
|
|
31
|
+
rows, events = [], []
|
|
32
|
+
paused_until = 0
|
|
33
|
+
|
|
34
|
+
for t in range(1, turns + 1):
|
|
35
|
+
key, val = f"param_{t}", str(rng.randint(10000, 99999))
|
|
36
|
+
truth[key] = val
|
|
37
|
+
for eng in systems.values():
|
|
38
|
+
eng.record(t, f"{key}={val}")
|
|
39
|
+
|
|
40
|
+
if t % dream_every == 0:
|
|
41
|
+
systems["in_place"].dream()
|
|
42
|
+
if t >= paused_until:
|
|
43
|
+
systems["dreamguard"].dream()
|
|
44
|
+
events.append({"turn": t, "type": "dream"})
|
|
45
|
+
|
|
46
|
+
dg = systems["dreamguard"]
|
|
47
|
+
manual = rollback_at is not None and t == rollback_at
|
|
48
|
+
auto = auto_rollback and t >= paused_until and dg.evaluate(truth) < threshold
|
|
49
|
+
if manual or auto:
|
|
50
|
+
dg.rollback()
|
|
51
|
+
paused_until = t + cooldown
|
|
52
|
+
events.append({"turn": t, "type": "rollback"})
|
|
53
|
+
|
|
54
|
+
rows.append({
|
|
55
|
+
"turn": t,
|
|
56
|
+
**{f"{n}_acc": round(100 * e.evaluate(truth), 1) for n, e in systems.items()},
|
|
57
|
+
**{f"{n}_chars": len(e.context()) for n, e in systems.items()},
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
return {"config": {"turns": turns, "dream_every": dream_every, "retention": retention,
|
|
61
|
+
"threshold": threshold, "cooldown": cooldown, "seed": seed,
|
|
62
|
+
"auto_rollback": auto_rollback, "rollback_at": rollback_at},
|
|
63
|
+
"rows": rows, "events": events}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
if __name__ == "__main__":
|
|
67
|
+
auto = "--auto" in sys.argv
|
|
68
|
+
res = run_benchmark(auto_rollback=auto, rollback_at=None if auto else 40)
|
|
69
|
+
print(f"{'turn':>4} {'raw':>6} {'in_place':>9} {'dreamguard':>11}")
|
|
70
|
+
for r in res["rows"]:
|
|
71
|
+
if r["turn"] % 5 == 0:
|
|
72
|
+
print(f"{r['turn']:>4} {r['raw_acc']:>5}% {r['in_place_acc']:>8}% {r['dreamguard_acc']:>10}%")
|
|
73
|
+
print("events:", res["events"])
|
|
74
|
+
if "--json" in sys.argv:
|
|
75
|
+
with open("benchmark_results.json", "w") as f:
|
|
76
|
+
json.dump(res, f, indent=2)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Command line: `dreamguard serve` | `dreamguard benchmark [--auto]`."""
|
|
2
|
+
import argparse
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main(argv=None):
|
|
7
|
+
p = argparse.ArgumentParser(prog="dreamguard", description="Reversible memory compilation for AI agents")
|
|
8
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
9
|
+
s = sub.add_parser("serve", help="start the dashboard + API")
|
|
10
|
+
s.add_argument("--host", default="127.0.0.1")
|
|
11
|
+
s.add_argument("--port", type=int, default=8000)
|
|
12
|
+
b = sub.add_parser("benchmark", help="run the offline simulation")
|
|
13
|
+
b.add_argument("--auto", action="store_true", help="auto-rollback instead of manual at turn 40")
|
|
14
|
+
args = p.parse_args(argv)
|
|
15
|
+
|
|
16
|
+
if args.cmd == "serve":
|
|
17
|
+
try:
|
|
18
|
+
import uvicorn
|
|
19
|
+
from .api import app
|
|
20
|
+
except ImportError:
|
|
21
|
+
sys.exit('Server extras missing. Install with: pip install "dreamguard[server]"')
|
|
22
|
+
uvicorn.run(app, host=args.host, port=args.port)
|
|
23
|
+
else:
|
|
24
|
+
from .benchmark import run_benchmark
|
|
25
|
+
res = run_benchmark(auto_rollback=args.auto, rollback_at=None if args.auto else 40)
|
|
26
|
+
print(f"{'turn':>4} {'raw':>6} {'in_place':>9} {'dreamguard':>11}")
|
|
27
|
+
for r in res["rows"]:
|
|
28
|
+
if r["turn"] % 5 == 0:
|
|
29
|
+
print(f"{r['turn']:>4} {r['raw_acc']:>5}% {r['in_place_acc']:>8}% {r['dreamguard_acc']:>10}%")
|
|
30
|
+
print("events:", res["events"])
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
if __name__ == "__main__":
|
|
34
|
+
main()
|