coffloader 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.
- coffloader-0.1.0/.gitignore +42 -0
- coffloader-0.1.0/CHANGELOG.md +15 -0
- coffloader-0.1.0/LICENSE +21 -0
- coffloader-0.1.0/PKG-INFO +201 -0
- coffloader-0.1.0/README.md +172 -0
- coffloader-0.1.0/pyproject.toml +48 -0
- coffloader-0.1.0/src/coffloader/__init__.py +17 -0
- coffloader-0.1.0/src/coffloader/backends/__init__.py +8 -0
- coffloader-0.1.0/src/coffloader/backends/base.py +23 -0
- coffloader-0.1.0/src/coffloader/backends/composite.py +41 -0
- coffloader-0.1.0/src/coffloader/backends/local.py +37 -0
- coffloader-0.1.0/src/coffloader/backends/memory.py +25 -0
- coffloader-0.1.0/src/coffloader/index/__init__.py +15 -0
- coffloader-0.1.0/src/coffloader/index/embeddings.py +181 -0
- coffloader-0.1.0/src/coffloader/index/fts.py +218 -0
- coffloader-0.1.0/src/coffloader/index/hybrid.py +80 -0
- coffloader-0.1.0/src/coffloader/py.typed +0 -0
- coffloader-0.1.0/src/coffloader/store.py +279 -0
- coffloader-0.1.0/src/coffloader/toc.py +67 -0
- coffloader-0.1.0/tests/__init__.py +0 -0
- coffloader-0.1.0/tests/test_basic.py +153 -0
- coffloader-0.1.0/tests/test_conversation_simulator.py +241 -0
- coffloader-0.1.0/tests/test_stage3_agent.py +190 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
dist/
|
|
9
|
+
*.egg-info/
|
|
10
|
+
*.egg
|
|
11
|
+
.eggs/
|
|
12
|
+
|
|
13
|
+
# Virtual environments
|
|
14
|
+
.venv/
|
|
15
|
+
venv/
|
|
16
|
+
ENV/
|
|
17
|
+
|
|
18
|
+
# IDE
|
|
19
|
+
.idea/
|
|
20
|
+
.vscode/
|
|
21
|
+
*.swp
|
|
22
|
+
*.swo
|
|
23
|
+
|
|
24
|
+
# Testing
|
|
25
|
+
.pytest_cache/
|
|
26
|
+
.coverage
|
|
27
|
+
htmlcov/
|
|
28
|
+
.mypy_cache/
|
|
29
|
+
|
|
30
|
+
# OS
|
|
31
|
+
.DS_Store
|
|
32
|
+
Thumbs.db
|
|
33
|
+
|
|
34
|
+
# Project specific
|
|
35
|
+
*.db
|
|
36
|
+
|
|
37
|
+
# Secrets
|
|
38
|
+
.env
|
|
39
|
+
|
|
40
|
+
# Build artifacts
|
|
41
|
+
*.whl
|
|
42
|
+
*.tar.gz
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [0.1.0] - 2026-06-07
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Initial release
|
|
9
|
+
- `Coffloader` main class with `write`, `search`, `read`, `inspect`, `delete` methods
|
|
10
|
+
- `MemoryBackend`, `LocalBackend`, `CompositeBackend` for flexible storage
|
|
11
|
+
- SQLite FTS5 index for BM25 keyword search
|
|
12
|
+
- Optional semantic search with sentence-transformers (`pip install coffloader[embed]`)
|
|
13
|
+
- Hybrid search combining BM25 + embeddings via Reciprocal Rank Fusion
|
|
14
|
+
- Size limits with reject or metadata-only modes
|
|
15
|
+
- Namespace filtering for multi-session/multi-agent isolation
|
coffloader-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 coffloader contributors
|
|
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: coffloader
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: External memory for AI agents — offload context to a VFS, index summaries, retrieve on demand.
|
|
5
|
+
Project-URL: Homepage, https://github.com/mingyk/coffloader
|
|
6
|
+
Project-URL: Repository, https://github.com/mingyk/coffloader
|
|
7
|
+
Author: coffloader contributors
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: agent,context,llm,memory,rag,vfs
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
24
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
25
|
+
Provides-Extra: embed
|
|
26
|
+
Requires-Dist: numpy>=1.21; extra == 'embed'
|
|
27
|
+
Requires-Dist: sentence-transformers>=2.2; extra == 'embed'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# coffloader
|
|
31
|
+
|
|
32
|
+
**External memory for AI agents** — offload context to a VFS, index caller-provided summaries, retrieve on demand.
|
|
33
|
+
|
|
34
|
+
[](https://www.python.org/downloads/)
|
|
35
|
+
[](LICENSE)
|
|
36
|
+
[](#status)
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install coffloader # core (BM25 search)
|
|
40
|
+
pip install coffloader[embed] # + semantic search (sentence-transformers)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## What it does
|
|
46
|
+
|
|
47
|
+
Agents accumulate context faster than any window allows. coffloader offloads content to storage, keeps a searchable index of summaries, and retrieves full content on demand.
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
write(content, summary) → store blob + index summary
|
|
51
|
+
search(query) → top-k summaries + addresses
|
|
52
|
+
read(address) → full content
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Key constraints:**
|
|
56
|
+
- `summary` is **required** on write — your agent/LLM provides it, not coffloader
|
|
57
|
+
- No LLM calls inside the library — pure storage and retrieval
|
|
58
|
+
- Caller handles contradiction detection, dedup, and reasoning
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Quick start
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from coffloader import Coffloader
|
|
66
|
+
|
|
67
|
+
store = Coffloader()
|
|
68
|
+
|
|
69
|
+
# 1. Offload a conversation segment (summary comes from your agent)
|
|
70
|
+
store.write(
|
|
71
|
+
content="[Turn 1] User: I was charged twice for order #9910...",
|
|
72
|
+
summary="Customer reports duplicate charge on order #9910",
|
|
73
|
+
metadata={"session_id": "ticket_8842", "segment": 1},
|
|
74
|
+
path="/sessions/ticket_8842/seg_001.txt",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# 2. Later: search when user asks about earlier context
|
|
78
|
+
hits = store.search("order number", namespace="/sessions/ticket_8842/")
|
|
79
|
+
|
|
80
|
+
# 3. Load full content and inject into your LLM
|
|
81
|
+
text = store.read_text(hits[0].address)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**The loop:** offload cold context → search when needed → read and inject.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## API
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
store = Coffloader(
|
|
92
|
+
backend=None, # default: in-memory VFS
|
|
93
|
+
max_bytes=512_000, # default: 512 KB — reject oversized payloads
|
|
94
|
+
on_oversize="reject", # "reject" or "metadata_only"
|
|
95
|
+
hybrid=True, # default: True — use BM25 + embeddings if available
|
|
96
|
+
min_similarity=0.3, # default: 0.3 — filter out weak embedding matches
|
|
97
|
+
# lower = more results, less relevant
|
|
98
|
+
# higher = fewer results, more relevant
|
|
99
|
+
# set to 0.0 to disable filtering
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Store content with a caller-provided summary
|
|
103
|
+
result = store.write(content, summary, metadata={}, path=None)
|
|
104
|
+
|
|
105
|
+
# Search indexed summaries (returns TocEntry list, not full content)
|
|
106
|
+
hits = store.search(query, k=5, filters={}, namespace=None)
|
|
107
|
+
# ^^^ number of results to return
|
|
108
|
+
|
|
109
|
+
# Load full content
|
|
110
|
+
data = store.read(address) # bytes
|
|
111
|
+
text = store.read_text(address) # str
|
|
112
|
+
|
|
113
|
+
# Check size before writing
|
|
114
|
+
check = store.inspect(content) # .acceptable, .byte_count
|
|
115
|
+
|
|
116
|
+
# Delete
|
|
117
|
+
store.delete(address)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**Defaults are exposed as class attributes:**
|
|
121
|
+
```python
|
|
122
|
+
Coffloader.DEFAULT_MAX_BYTES # 512_000
|
|
123
|
+
Coffloader.DEFAULT_MIN_SIMILARITY # 0.3
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Composite backends
|
|
129
|
+
|
|
130
|
+
Route paths to different storage:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from coffloader import Coffloader, CompositeBackend, LocalBackend, MemoryBackend
|
|
134
|
+
|
|
135
|
+
store = Coffloader(
|
|
136
|
+
backend=CompositeBackend(
|
|
137
|
+
default=MemoryBackend(),
|
|
138
|
+
routes={"/archive/": LocalBackend(root="./data")},
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Patterns
|
|
146
|
+
|
|
147
|
+
**Long session (segmented):** Offload every ~15 turns. Search returns precise segments, not the whole transcript.
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
store.write(content=turns_1_15, summary="...", path="/sessions/abc/seg_001.txt")
|
|
151
|
+
store.write(content=turns_16_30, summary="...", path="/sessions/abc/seg_002.txt")
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**Tool output:** Offload large grep/API results with a structural summary (no LLM needed).
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
store.write(
|
|
158
|
+
content=grep_output,
|
|
159
|
+
summary=f"grep error src/ → {n} matches",
|
|
160
|
+
path=f"/active/{session}/tool_001.txt",
|
|
161
|
+
)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**Multi-agent:** Use namespaces for isolation (`/agent/{id}/`) or sharing (`/shared/`).
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Limits
|
|
169
|
+
|
|
170
|
+
- Max payload: 512 KB by default (configurable)
|
|
171
|
+
- Oversized content is rejected or recorded as metadata-only
|
|
172
|
+
- No silent truncation
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Status
|
|
177
|
+
|
|
178
|
+
Pre-alpha. Core API is stable: `write`, `search`, `read`, `inspect`, `delete`.
|
|
179
|
+
|
|
180
|
+
**Working:**
|
|
181
|
+
- BM25 (keyword) search via SQLite FTS5
|
|
182
|
+
- Semantic search via `[embed]` optional extra
|
|
183
|
+
- Hybrid search (BM25 + embeddings) with Reciprocal Rank Fusion
|
|
184
|
+
|
|
185
|
+
**Not yet implemented:**
|
|
186
|
+
- Persistent index to disk
|
|
187
|
+
- Sharded TOC for large corpora
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Non-goals
|
|
192
|
+
|
|
193
|
+
- LLM calls from the library
|
|
194
|
+
- Automatic dedup, contradiction detection, or memory merge
|
|
195
|
+
- Knowledge graphs or hierarchical rollups
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## License
|
|
200
|
+
|
|
201
|
+
MIT
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# coffloader
|
|
2
|
+
|
|
3
|
+
**External memory for AI agents** — offload context to a VFS, index caller-provided summaries, retrieve on demand.
|
|
4
|
+
|
|
5
|
+
[](https://www.python.org/downloads/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](#status)
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install coffloader # core (BM25 search)
|
|
11
|
+
pip install coffloader[embed] # + semantic search (sentence-transformers)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## What it does
|
|
17
|
+
|
|
18
|
+
Agents accumulate context faster than any window allows. coffloader offloads content to storage, keeps a searchable index of summaries, and retrieves full content on demand.
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
write(content, summary) → store blob + index summary
|
|
22
|
+
search(query) → top-k summaries + addresses
|
|
23
|
+
read(address) → full content
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**Key constraints:**
|
|
27
|
+
- `summary` is **required** on write — your agent/LLM provides it, not coffloader
|
|
28
|
+
- No LLM calls inside the library — pure storage and retrieval
|
|
29
|
+
- Caller handles contradiction detection, dedup, and reasoning
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Quick start
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from coffloader import Coffloader
|
|
37
|
+
|
|
38
|
+
store = Coffloader()
|
|
39
|
+
|
|
40
|
+
# 1. Offload a conversation segment (summary comes from your agent)
|
|
41
|
+
store.write(
|
|
42
|
+
content="[Turn 1] User: I was charged twice for order #9910...",
|
|
43
|
+
summary="Customer reports duplicate charge on order #9910",
|
|
44
|
+
metadata={"session_id": "ticket_8842", "segment": 1},
|
|
45
|
+
path="/sessions/ticket_8842/seg_001.txt",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# 2. Later: search when user asks about earlier context
|
|
49
|
+
hits = store.search("order number", namespace="/sessions/ticket_8842/")
|
|
50
|
+
|
|
51
|
+
# 3. Load full content and inject into your LLM
|
|
52
|
+
text = store.read_text(hits[0].address)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**The loop:** offload cold context → search when needed → read and inject.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## API
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
store = Coffloader(
|
|
63
|
+
backend=None, # default: in-memory VFS
|
|
64
|
+
max_bytes=512_000, # default: 512 KB — reject oversized payloads
|
|
65
|
+
on_oversize="reject", # "reject" or "metadata_only"
|
|
66
|
+
hybrid=True, # default: True — use BM25 + embeddings if available
|
|
67
|
+
min_similarity=0.3, # default: 0.3 — filter out weak embedding matches
|
|
68
|
+
# lower = more results, less relevant
|
|
69
|
+
# higher = fewer results, more relevant
|
|
70
|
+
# set to 0.0 to disable filtering
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Store content with a caller-provided summary
|
|
74
|
+
result = store.write(content, summary, metadata={}, path=None)
|
|
75
|
+
|
|
76
|
+
# Search indexed summaries (returns TocEntry list, not full content)
|
|
77
|
+
hits = store.search(query, k=5, filters={}, namespace=None)
|
|
78
|
+
# ^^^ number of results to return
|
|
79
|
+
|
|
80
|
+
# Load full content
|
|
81
|
+
data = store.read(address) # bytes
|
|
82
|
+
text = store.read_text(address) # str
|
|
83
|
+
|
|
84
|
+
# Check size before writing
|
|
85
|
+
check = store.inspect(content) # .acceptable, .byte_count
|
|
86
|
+
|
|
87
|
+
# Delete
|
|
88
|
+
store.delete(address)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Defaults are exposed as class attributes:**
|
|
92
|
+
```python
|
|
93
|
+
Coffloader.DEFAULT_MAX_BYTES # 512_000
|
|
94
|
+
Coffloader.DEFAULT_MIN_SIMILARITY # 0.3
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Composite backends
|
|
100
|
+
|
|
101
|
+
Route paths to different storage:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from coffloader import Coffloader, CompositeBackend, LocalBackend, MemoryBackend
|
|
105
|
+
|
|
106
|
+
store = Coffloader(
|
|
107
|
+
backend=CompositeBackend(
|
|
108
|
+
default=MemoryBackend(),
|
|
109
|
+
routes={"/archive/": LocalBackend(root="./data")},
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Patterns
|
|
117
|
+
|
|
118
|
+
**Long session (segmented):** Offload every ~15 turns. Search returns precise segments, not the whole transcript.
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
store.write(content=turns_1_15, summary="...", path="/sessions/abc/seg_001.txt")
|
|
122
|
+
store.write(content=turns_16_30, summary="...", path="/sessions/abc/seg_002.txt")
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**Tool output:** Offload large grep/API results with a structural summary (no LLM needed).
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
store.write(
|
|
129
|
+
content=grep_output,
|
|
130
|
+
summary=f"grep error src/ → {n} matches",
|
|
131
|
+
path=f"/active/{session}/tool_001.txt",
|
|
132
|
+
)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Multi-agent:** Use namespaces for isolation (`/agent/{id}/`) or sharing (`/shared/`).
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Limits
|
|
140
|
+
|
|
141
|
+
- Max payload: 512 KB by default (configurable)
|
|
142
|
+
- Oversized content is rejected or recorded as metadata-only
|
|
143
|
+
- No silent truncation
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Status
|
|
148
|
+
|
|
149
|
+
Pre-alpha. Core API is stable: `write`, `search`, `read`, `inspect`, `delete`.
|
|
150
|
+
|
|
151
|
+
**Working:**
|
|
152
|
+
- BM25 (keyword) search via SQLite FTS5
|
|
153
|
+
- Semantic search via `[embed]` optional extra
|
|
154
|
+
- Hybrid search (BM25 + embeddings) with Reciprocal Rank Fusion
|
|
155
|
+
|
|
156
|
+
**Not yet implemented:**
|
|
157
|
+
- Persistent index to disk
|
|
158
|
+
- Sharded TOC for large corpora
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Non-goals
|
|
163
|
+
|
|
164
|
+
- LLM calls from the library
|
|
165
|
+
- Automatic dedup, contradiction detection, or memory merge
|
|
166
|
+
- Knowledge graphs or hierarchical rollups
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## License
|
|
171
|
+
|
|
172
|
+
MIT
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "coffloader"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "External memory for AI agents — offload context to a VFS, index summaries, retrieve on demand."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [{ name = "coffloader contributors" }]
|
|
13
|
+
keywords = ["llm", "agent", "memory", "context", "rag", "vfs"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.9",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
dependencies = []
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
embed = ["sentence-transformers>=2.2", "numpy>=1.21"]
|
|
30
|
+
dev = ["pytest>=8.0", "ruff>=0.4", "mypy>=1.10"]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/mingyk/coffloader"
|
|
34
|
+
Repository = "https://github.com/mingyk/coffloader"
|
|
35
|
+
|
|
36
|
+
[tool.hatch.build.targets.wheel]
|
|
37
|
+
packages = ["src/coffloader"]
|
|
38
|
+
|
|
39
|
+
[tool.ruff]
|
|
40
|
+
line-length = 100
|
|
41
|
+
target-version = "py39"
|
|
42
|
+
|
|
43
|
+
[tool.ruff.lint]
|
|
44
|
+
select = ["E", "F", "I", "UP"]
|
|
45
|
+
|
|
46
|
+
[tool.mypy]
|
|
47
|
+
python_version = "3.9"
|
|
48
|
+
strict = true
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""coffloader — External memory for AI agents."""
|
|
2
|
+
|
|
3
|
+
from .backends import CompositeBackend, LocalBackend, MemoryBackend
|
|
4
|
+
from .store import Coffloader
|
|
5
|
+
from .toc import InspectResult, TocEntry, WriteResult
|
|
6
|
+
|
|
7
|
+
__version__ = "0.1.0"
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"Coffloader",
|
|
11
|
+
"TocEntry",
|
|
12
|
+
"WriteResult",
|
|
13
|
+
"InspectResult",
|
|
14
|
+
"CompositeBackend",
|
|
15
|
+
"LocalBackend",
|
|
16
|
+
"MemoryBackend",
|
|
17
|
+
]
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Storage backends for coffloader."""
|
|
2
|
+
|
|
3
|
+
from .base import BackendProtocol
|
|
4
|
+
from .composite import CompositeBackend
|
|
5
|
+
from .local import LocalBackend
|
|
6
|
+
from .memory import MemoryBackend
|
|
7
|
+
|
|
8
|
+
__all__ = ["BackendProtocol", "CompositeBackend", "LocalBackend", "MemoryBackend"]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Backend protocol for VFS storage."""
|
|
2
|
+
|
|
3
|
+
from typing import Protocol
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BackendProtocol(Protocol):
|
|
7
|
+
"""Interface for storage backends."""
|
|
8
|
+
|
|
9
|
+
def write(self, path: str, data: bytes) -> None:
|
|
10
|
+
"""Store data at the given path."""
|
|
11
|
+
...
|
|
12
|
+
|
|
13
|
+
def read(self, path: str) -> bytes:
|
|
14
|
+
"""Read data from the given path. Raises KeyError if not found."""
|
|
15
|
+
...
|
|
16
|
+
|
|
17
|
+
def delete(self, path: str) -> bool:
|
|
18
|
+
"""Delete data at the given path. Returns True if deleted, False if not found."""
|
|
19
|
+
...
|
|
20
|
+
|
|
21
|
+
def exists(self, path: str) -> bool:
|
|
22
|
+
"""Check if path exists."""
|
|
23
|
+
...
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Composite backend that routes by path prefix."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .base import BackendProtocol
|
|
6
|
+
from .memory import MemoryBackend
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CompositeBackend:
|
|
10
|
+
"""Route paths to different backends based on prefix.
|
|
11
|
+
|
|
12
|
+
Longest matching prefix wins. Unmatched paths go to the default backend.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
default: BackendProtocol | None = None,
|
|
18
|
+
routes: dict[str, BackendProtocol] | None = None,
|
|
19
|
+
) -> None:
|
|
20
|
+
self._default: BackendProtocol = default or MemoryBackend()
|
|
21
|
+
self._routes = routes or {}
|
|
22
|
+
# Sort routes by length descending for longest-prefix matching
|
|
23
|
+
self._sorted_prefixes = sorted(self._routes.keys(), key=len, reverse=True)
|
|
24
|
+
|
|
25
|
+
def _get_backend(self, path: str) -> BackendProtocol:
|
|
26
|
+
for prefix in self._sorted_prefixes:
|
|
27
|
+
if path.startswith(prefix):
|
|
28
|
+
return self._routes[prefix]
|
|
29
|
+
return self._default
|
|
30
|
+
|
|
31
|
+
def write(self, path: str, data: bytes) -> None:
|
|
32
|
+
self._get_backend(path).write(path, data)
|
|
33
|
+
|
|
34
|
+
def read(self, path: str) -> bytes:
|
|
35
|
+
return self._get_backend(path).read(path)
|
|
36
|
+
|
|
37
|
+
def delete(self, path: str) -> bool:
|
|
38
|
+
return self._get_backend(path).delete(path)
|
|
39
|
+
|
|
40
|
+
def exists(self, path: str) -> bool:
|
|
41
|
+
return self._get_backend(path).exists(path)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Local filesystem storage backend."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class LocalBackend:
|
|
7
|
+
"""Store blobs on local disk under a root directory."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, root: str | Path) -> None:
|
|
10
|
+
self._root = Path(root).resolve()
|
|
11
|
+
self._root.mkdir(parents=True, exist_ok=True)
|
|
12
|
+
|
|
13
|
+
def _resolve(self, path: str) -> Path:
|
|
14
|
+
# Strip leading slash for joining
|
|
15
|
+
relative = path.lstrip("/")
|
|
16
|
+
return self._root / relative
|
|
17
|
+
|
|
18
|
+
def write(self, path: str, data: bytes) -> None:
|
|
19
|
+
file_path = self._resolve(path)
|
|
20
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
21
|
+
file_path.write_bytes(data)
|
|
22
|
+
|
|
23
|
+
def read(self, path: str) -> bytes:
|
|
24
|
+
file_path = self._resolve(path)
|
|
25
|
+
if not file_path.exists():
|
|
26
|
+
raise KeyError(f"Path not found: {path}")
|
|
27
|
+
return file_path.read_bytes()
|
|
28
|
+
|
|
29
|
+
def delete(self, path: str) -> bool:
|
|
30
|
+
file_path = self._resolve(path)
|
|
31
|
+
if file_path.exists():
|
|
32
|
+
file_path.unlink()
|
|
33
|
+
return True
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
def exists(self, path: str) -> bool:
|
|
37
|
+
return self._resolve(path).exists()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""In-memory storage backend."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class MemoryBackend:
|
|
5
|
+
"""Store blobs in a Python dict. Data lost on process exit."""
|
|
6
|
+
|
|
7
|
+
def __init__(self) -> None:
|
|
8
|
+
self._store: dict[str, bytes] = {}
|
|
9
|
+
|
|
10
|
+
def write(self, path: str, data: bytes) -> None:
|
|
11
|
+
self._store[path] = data
|
|
12
|
+
|
|
13
|
+
def read(self, path: str) -> bytes:
|
|
14
|
+
if path not in self._store:
|
|
15
|
+
raise KeyError(f"Path not found: {path}")
|
|
16
|
+
return self._store[path]
|
|
17
|
+
|
|
18
|
+
def delete(self, path: str) -> bool:
|
|
19
|
+
if path in self._store:
|
|
20
|
+
del self._store[path]
|
|
21
|
+
return True
|
|
22
|
+
return False
|
|
23
|
+
|
|
24
|
+
def exists(self, path: str) -> bool:
|
|
25
|
+
return path in self._store
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Index implementations for TOC search."""
|
|
2
|
+
|
|
3
|
+
from .fts import FTSIndex
|
|
4
|
+
|
|
5
|
+
# Optional imports for embedding-based search
|
|
6
|
+
try:
|
|
7
|
+
from .embeddings import EmbeddingIndex
|
|
8
|
+
from .hybrid import HybridIndex
|
|
9
|
+
EMBEDDINGS_AVAILABLE = True
|
|
10
|
+
except ImportError:
|
|
11
|
+
EMBEDDINGS_AVAILABLE = False
|
|
12
|
+
EmbeddingIndex = None # type: ignore
|
|
13
|
+
HybridIndex = None # type: ignore
|
|
14
|
+
|
|
15
|
+
__all__ = ["FTSIndex", "EmbeddingIndex", "HybridIndex", "EMBEDDINGS_AVAILABLE"]
|