Spaces:
Running
Running
File size: 13,425 Bytes
12a6c9a 7340590 12a6c9a 7340590 12a6c9a 7340590 12a6c9a 7340590 12a6c9a 7340590 12a6c9a 7340590 12a6c9a 7340590 12a6c9a 7340590 12a6c9a 7340590 12a6c9a 7340590 12a6c9a 7340590 12a6c9a 7340590 12a6c9a 7340590 12a6c9a 7340590 12a6c9a 7340590 12a6c9a 7340590 12a6c9a 7340590 12a6c9a 7340590 12a6c9a 7340590 12a6c9a 7340590 12a6c9a 7340590 12a6c9a 7340590 12a6c9a 7340590 12a6c9a 7340590 12a6c9a 7340590 12a6c9a 7340590 12a6c9a 7340590 12a6c9a 7340590 12a6c9a 7340590 12a6c9a 7340590 12a6c9a 7340590 12a6c9a 7340590 12a6c9a 7340590 12a6c9a 7340590 12a6c9a 7340590 12a6c9a 7340590 12a6c9a 7340590 12a6c9a 7340590 12a6c9a 7340590 12a6c9a 7340590 12a6c9a 7340590 12a6c9a 7340590 12a6c9a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 | # agentcache-python β Agent Instructions
## What this project is
A Python REST + WebSocket + MCP memory server backed by SQLite. No Node.js, no iii-engine, no Dolt. Agents use it to store observations scoped to `(folder_path, agent_id)` pairs and global long-term memories. The viewer, MCP tools, and REST API are built around the **folder-based memory model** β sessions, lessons, slots, and actions are removed.
## Project layout
```
src/
βββ app.py Thin Flask factory β create_app(), WebSocket, CORS hook
βββ cli.py CLI entrypoint (agentcache serve/migrate/export)
βββ connect.py Client connection helper
βββ db.py StateKV β SQLite WAL, per-thread connections, stats()
βββ functions.py All canonical business logic (large; memory/ shims delegate here)
βββ search.py BM25 + VectorIndex + HybridSearch + 3 embedding providers
βββ viewer_helpers.py Viewer HTML injection helper
βββ workers.py Background threads β index rebuild, auto-forget, SIGTERM handler
β
βββ routes/ Flask blueprints
β βββ observations.py /observe, /agent/observe, /folders, /folder/observations
β βββ memories.py /remember, /agent/remember, /memories, /forget
β βββ search.py /search, /timeline
β βββ graph.py /graph, /graph/stats, /graph/query, /graph/build
β βββ health.py /livez, /health, /audit, /config/flags
β βββ mcp.py /mcp/tools GET+POST (12 active tools)
β βββ migration.py /migrate
β
βββ memory/ Thin shim package β delegates to functions.py
β βββ observe.py folder_observe, observe, build_synthetic_compression, strip_private_data
β βββ remember.py remember, forget, jaccard_similarity
β βββ context.py context, export_data, rebuild_index
β βββ graph.py folder_graph_build
β βββ timeline.py folder_timeline, folder_search
β βββ health.py health_check, auto_forget
β
βββ storage/ KV scope registry + path/ID utilities
β βββ scopes.py KV class (mirrored from functions.py)
β βββ paths.py normalize_folder_path, validate_agent_id, generate_id, fingerprint_id
β βββ images.py save_image_to_disk, delete_image, touch_image
β
βββ viewer/
βββ index.html Single-file HTML dashboard (4 tabs: Folders / Memories / Graph / Timeline)
sync.py HuggingFace dataset backup/restore
Dockerfile HF Space container definition
start.sh Boot: restore DB β start server β start sync loop
requirements.txt flask, flask-sock, requests, websockets, python-dateutil, huggingface_hub
pyproject.toml pip-installable package (agentcache==0.9.8, Python β₯3.10)
tests/ pytest suite β unit, integration, and Hypothesis property tests
```
## Running
```bash
pip install -r requirements.txt
python src/app.py
# Server on http://localhost:3111
# Viewer at http://localhost:3111/viewer
```
No build step. No external database. SQLite file lives at `~/.agentcache/agentcache.db`.
## Architecture
### Data Model β Folder-Based Memory
The primary unit of storage is `(folder_path, agent_id)`. Each agent accumulates observations scoped to the folder it is working in. Global long-term memories remain unchanged.
### Storage β `src/db.py`
`StateKV` wraps a single SQLite file with:
- `kv_store(scope TEXT, key TEXT, value TEXT, PRIMARY KEY(scope, key))` β all data as JSON
- `audit_log(id, ts, agent_id, message)` β write audit trail
- `sync_state_metadata(key, value)` β HuggingFace sync watermark
Per-thread persistent connections via `threading.local()`. WAL checkpoint registered via `atexit` and on `SIGTERM`/`SIGINT`.
Key scopes (defined in `functions.py` `KV` class and mirrored in `src/storage/scopes.py`):
| Scope | Content |
|-------|---------|
| `mem:folders` | Index of all `(folder_path, agent_id)` pairs β key = `"{path}:{agent}"` |
| `mem:folder:{path}:{agent}` | Observations for a pair β key = obs_id |
| `mem:foldermeta:{path}:{agent}` | Metadata for a pair (obsCount, lastUpdated, summary) |
| `mem:obs_lookup` | O(1) reverse-lookup: obs_id β `{folderPath, agentId}` |
| `mem:memories` | Global long-term memories |
| `mem:index:bm25:*` | Sharded BM25 index (2MB chunks) |
| `mem:audit` | Audit log entries |
| `mem:relations` | Knowledge graph edges |
| `mem:sessions` | Legacy session store (read-only, migration only) |
| `mem:obs:{session_id}` | Legacy per-session observations (read-only, migration only) |
### Business logic β `src/functions.py`
All canonical implementations live here. `src/memory/*` are thin shims that re-export from this module (for future decoupling).
**Observation pipeline:**
```
raw payload β normalize_folder_path() β validate_agent_id() β strip_private_data()
β build obs dict β kv.set(folder_obs scope) β upsert folder_meta + folders index
β kv.set(obs_lookup) β BM25-indexed β vector-indexed (if key set)
β schedule_save() (debounced 5s) β audit log β WebSocket broadcast
```
**Memory versioning:** `remember()` checks Jaccard similarity against existing memories. If > 0.7 match found, new memory supersedes old (`isLatest=False` on old, `parentId` set on new).
**Search:** `folder_search()` uses `HybridSearch` (BM25 + vector, RRF k=60). Falls back to BM25-only when no embedding provider is configured. Results include both folder observations and global memories.
**`health_check()`** returns: `folderCount`, `agentCount`, `pairCount`, `observationCount`, `memoryCount`, `bm25IndexSize`, `vectorIndexSize`, `dbPath`, plus `db.stats()` fields.
### Search β `src/search.py`
- `SearchIndex`: BM25 with Porter stemmer and synonym expansion. Dirty-flag (`_dirty`) prevents unnecessary saves. Persisted in sharded 2MB KV chunks.
- `VectorIndex`: cosine similarity over embeddings stored as base64-encoded float32 arrays. Also has `_dirty` flag.
- `HybridSearch`: fuses BM25 + vector scores with RRF (k=60).
**Embedding providers** (auto-selected by priority in `create_app()`):
| Priority | Provider | Env var | Dimensions |
|----------|----------|---------|------------|
| 1 | `GeminiEmbeddingProvider` | `GEMINI_API_KEY` / `GOOGLE_API_KEY` | 768 |
| 2 | `OpenAIEmbeddingProvider` | `OPENAI_API_KEY` | 1536 |
| 3 | `SentenceTransformerProvider` | `AGENTCACHE_LOCAL_EMBEDDING_MODEL` | variable |
| 4 | BM25-only | β | β |
### Server β `src/app.py`
Boot order:
1. Load `~/.agentcache/.env` if present
2. Initialize `StateKV` (SQLite)
3. Auto-select embedding provider (Gemini β OpenAI β SentenceTransformer β BM25-only)
4. Initialize `IndexPersistence` (load or rebuild)
5. Backfill `obs_lookup` index if missing
6. Create Flask app, register blueprints
7. Set up WebSocket `/stream/mem-live/viewer`
8. Register CORS `after_request` hook
9. Start background workers (index rebuild if empty/stale, auto-forget loop)
Auth: all endpoints check `AGENTCACHE_SECRET` via `hmac.compare_digest`. `/livez` is always open.
## MCP Tools
The server exposes **12 MCP tools** via `GET /agentcache/mcp/tools` and `POST /agentcache/mcp/tools`.
| Tool | Description | Status |
|------|-------------|--------|
| `agent_observe` | Log observation to a `(folderPath, agentId)` pair | Working |
| `agent_remember` | Save to global long-term memory | Working |
| `memory_recall` | Search folder obs + global memories (BM25+vector) | Working |
| `memory_smart_search` | Hybrid semantic+keyword search (alias for recall) | Working |
| `memory_save` | Explicitly save insight to long-term memory | Working |
| `memory_export` | Export all data as JSON (v2 format) | Working |
| `memory_forget` | Delete memory or folder pair observations | Working |
| `memory_diagnose` | Health check (folder/agent/obs/memory counts) | Working |
| `memory_folders` | List all `(folder, agent)` pairs | Working |
| `memory_folder_observations` | Get observations for a specific pair | Working |
| `memory_timeline` | Folder activity feed (sorted by time, filterable) | Working |
**MCP stdio wrapper:** `src/mcp_stdio.py` reads `AGENTCACHE_URL` and `AGENTCACHE_SECRET` from environment variables dynamically.
## Consistency rules
**When adding a REST endpoint:**
1. Add the route in the appropriate `src/routes/*.py` blueprint
2. Update the `API Reference` section in `README.md`
3. Add the MCP tool in `src/routes/mcp.py` if it should be agent-callable
**When adding an MCP tool:**
1. Add the schema to the `GET /mcp/tools` response in `src/routes/mcp.py`
2. Add the handler case to the `POST /mcp/tools` dispatch in `src/routes/mcp.py`
3. Update the tool table in `README.md`
4. Update `AGENTS.md` tool list
**When changing data scopes:**
1. Update the `KV` class in `src/functions.py` (canonical)
2. Mirror the change in `src/storage/scopes.py`
3. Update the scope table in this file
## Code patterns
### Adding a new KV scope
```python
# In src/functions.py KV class (canonical):
class KV:
your_scope = "mem:your-scope"
@staticmethod
def your_dynamic_scope(id: str) -> str:
return f"mem:your-scope:{id}"
```
### Adding a REST endpoint
```python
# In the appropriate src/routes/*.py blueprint:
@your_bp.route('/agentcache/your-path', methods=['POST'])
def your_endpoint():
auth_err = _check_auth()
if auth_err:
return auth_err
body = request.get_json(force=True) or {}
# validate fields explicitly β never pass raw body to functions
result = functions.your_function(_get_kv(), body.get('field'))
return jsonify(result), 200
```
### Adding an MCP tool schema
In `src/routes/mcp.py`, `GET /agentcache/mcp/tools` handler:
```python
{
"name": "memory_your_tool",
"description": "What it does",
"inputSchema": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "..."}
},
"required": ["query"]
}
}
```
In `src/routes/mcp.py`, `POST /agentcache/mcp/tools` handler:
```python
elif tool_name == 'memory_your_tool':
query = args.get('query', '')
result = functions.your_function(kv, query)
return jsonify({'content': [{'type': 'text', 'text': json.dumps(result)}]})
```
## Environment variables
| Variable | Default | Purpose |
|----------|---------|---------|
| `III_REST_PORT` / `PORT` | `3111` | Server port |
| `GEMINI_API_KEY` / `GOOGLE_API_KEY` | β | Enables Gemini vector search (priority 1) |
| `OPENAI_API_KEY` | β | Enables OpenAI vector search (priority 2) |
| `AGENTCACHE_LOCAL_EMBEDDING_MODEL` | β | SentenceTransformer model name (priority 3) |
| `AGENTCACHE_SECRET` | β | Bearer token auth |
| `AGENT_ID` | β | Default agent ID |
| `AGENTCACHE_AGENT_SCOPE=isolated` | β | Filter data to current agent |
| `AGENTCACHE_CWD` | β | Fallback folder path for legacy clients |
| `MAX_OBS_PER_FOLDER` | `2000` | Observations hard cap per (folder, agent) pair |
| `TOKEN_BUDGET` | `2000` | Context compilation cap |
| `GRAPH_EXTRACTION_ENABLED` | `false` | Graph extraction (needs LLM) |
| `CONSOLIDATION_ENABLED` | `false` | Consolidation (needs LLM) |
| `AGENTCACHE_AUTO_COMPRESS` | `false` | LLM compression |
| `AUTO_FORGET_ENABLED` | β | Auto-forget sweep (set to "false" to disable) |
| `AGENTCACHE_CORS_ORIGINS` | see app.py | Comma-separated allowed origins |
| `AGENTCACHE_IMAGE_STORE_MAX_BYTES` | 500MB | Image store byte limit |
| `HF_TOKEN` | β | HuggingFace sync |
| `AGENTCACHE_DATASET_REPO` | β | HF dataset repo for backup |
## Testing
```bash
pip install -e ".[dev]"
pytest tests/ -v
```
Tests live in `tests/` β 17 test files covering unit tests, integration tests (Flask test client), and Hypothesis property tests.
Key test files:
- `tests/test_properties.py` β 8 correctness properties (pair isolation, obs count consistency, index coverage, privacy, timeline ordering, forget completeness, memory version uniqueness, path normalization idempotency)
- `tests/test_api.py` β Flask test client integration tests
- `tests/test_route_regressions.py` β regression suite after blueprint split
## HuggingFace Space deployment
Data flow: `agentcache.db` (SQLite) β `sync.py` β HF dataset repo.
`start.sh` sequence:
1. Restore `agentcache.db` from dataset repo
2. Start `python src/app.py` in background
3. Run `sync.py` in a loop (backup every ~60s if changed)
## Viewer β `src/viewer/index.html`
Single-file HTML dashboard, served directly by Flask at `/viewer`. No build step, no bundler.
Tabs: **Folders**, **Memories**, **Graph**, **Timeline**.
- **Folders tab**: lists all `(folder, agent)` pairs; click a row to drill into observations
- **Memories tab**: global long-term memories with search
- **Graph tab**: force-directed graph β nodes = folder paths, edges = same-parent / cross-ref / agent-shared
- **Timeline tab**: all observations sorted by timestamp desc, filterable by folder path and agent ID
|