Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit5113f70

Browse files
committed
feat: harden adapters and storage controls
1 parentf95c34c commit5113f70

File tree

9 files changed

+249
-14
lines changed

9 files changed

+249
-14
lines changed

‎.github/workflows/ci.yml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ jobs:
3232
python -m pip install --upgrade pip
3333
pip install -e .[dev,test]
3434

35+
- name: Dependency audit (pip-audit)
36+
run: |
37+
python -m pip install pip-audit
38+
pip-audit
39+
3540
- name: Ruff (lint + format check)
3641
run: ruff check .
3742

‎README.md‎

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ uvicorn neuralcache.api.server_plus:app --port 8081 --reload
7171
- **Stigmergic pheromones** reward useful documents but decay over time, preventing filter bubbles.
7272
- **MMR + ε-greedy** introduces diversity without tanking relevance.
7373
- **Zero external dependencies by default.** Uses a hashing trick for embeddings so you can see results instantly, but slots in any vector model when you’re ready.
74-
- **Adapters included.** LangChain and LlamaIndex adapters ship in `neuralcache.adapters` and only import their extras when you use them.
74+
- **Adapters included.** LangChain and LlamaIndex adapters ship in `neuralcache.adapters`; install them on demand with `pip install "neuralcache[adapters]"`.
7575
- **CLI + REST API + FastAPI docs** give you multiple ways to integrate and debug.
7676
- **Plus API** adds `/rerank/batch` and Prometheus-ready `/metrics` endpoints when you run `uvicorn neuralcache.api.server_plus:app` (install the `neuralcache[ops]` extra for dependencies).
7777
- **SQLite persistence out of the box.** `neuralcache.storage.sqlite_state.SQLiteState` keeps narrative + pheromone state durable across workers without JSON file juggling.
@@ -117,13 +117,63 @@ Gating plugs in before narrative, pheromone, and MMR scoring—so downstream mem
117117
- **REST API** (`uvicorn neuralcache.api.server:app`) with `/rerank`, `/feedback`, `/metrics`, and `/healthz` endpoints.
118118
- **Plus API** (`uvicorn neuralcache.api.server_plus:app`) adds `/rerank/batch`, Prometheus `/metrics`, and mounts the legacy routes under `/v1`.
119119
- **CLI** (`neuralcache "<query>" docs.jsonl --top-k 5`) for quick experiments and scripting.
120-
- **LangChain adapter**: `from neuralcache.adapters import NeuralCacheLangChainReranker`
121-
- **LlamaIndex adapter**: `from neuralcache.adapters import NeuralCacheLlamaIndexReranker`
120+
- **LangChain adapter** (`pip install "neuralcache[adapters]"`): `from neuralcache.adapters import NeuralCacheLangChainReranker`
121+
- **LlamaIndex adapter** (`pip install "neuralcache[adapters]"`): `from neuralcache.adapters import NeuralCacheLlamaIndexReranker`
122122

123123
See [`examples/quickstart.py`](examples/quickstart.py) for an end-to-end script.
124124

125125
---
126126

127+
## Feedback API: closing the loop
128+
129+
Send successful reranks back to NeuralCache so the narrative EMA and pheromone
130+
signals keep learning:
131+
132+
```http
133+
POST /feedback
134+
Content-Type: application/json
135+
136+
{
137+
"query": "How do I rotate API keys?",
138+
"selected_ids": ["doc-42", "doc-71"],
139+
"success": 0.9,
140+
"best_doc_text": "Rotate keys via the dashboard > API tokens",
141+
"best_doc_embedding": [0.01, 0.32, -0.55, ...]
142+
}
143+
```
144+
145+
- `selected_ids` **must** match the `id` values returned by `/rerank`. The API
146+
rejects unknown IDs to prevent stale writes.
147+
- `success` scores the overall outcome (`1.0` for complete resolution, `0.0`
148+
for failure). Values below `settings.narrative_success_gate` are ignored for
149+
narrative updates but still count toward pheromone decay.
150+
- `best_doc_text`/`best_doc_embedding` are optional hints that let the reranker
151+
update the narrative vector even when a caller reformats the answer before
152+
returning it to the user.
153+
154+
On success the endpoint responds with `{"status": "ok"}`.
155+
156+
Tip: throttle feedback submissions with a short debounce window (e.g., only send
157+
feedback after end-users click “helpful”) to avoid promoting documents for noisy
158+
sessions.
159+
160+
---
161+
162+
## Privacy & retention tips
163+
164+
- Set `NEURALCACHE_STORAGE_PERSISTENCE_ENABLED=false` to run fully in-memory. Narrative
165+
vectors and pheromones reset on process restart and never touch disk.
166+
- Configure `NEURALCACHE_STORAGE_RETENTION_DAYS` (e.g., `7`) to purge pheromones and
167+
narrative state older than the retention window on startup. SQLite purges directly
168+
via `metadata`/`pheromones`, and the JSON fallback trims files in place.
169+
- Rotate SQLite files regularly or place them on encrypted storage. Review
170+
[`SECURITY.md`](SECURITY.md) for reporting procedures and deployment guardrails.
171+
172+
These controls let you scope how long user-derived signals persist while still
173+
benefiting from adaptive reranking.
174+
175+
---
176+
127177
## Configuration essentials
128178

129179
| Env var | Purpose | Default |
@@ -134,6 +184,8 @@ See [`examples/quickstart.py`](examples/quickstart.py) for an end-to-end script.
134184
| `NEURALCACHE_MAX_DOCUMENTS` | Safety cap on rerank set size | `128` |
135185
| `NEURALCACHE_MAX_TEXT_LENGTH` | Hard limit on document length (characters) | `8192` |
136186
| `NEURALCACHE_STORAGE_DIR` | Where SQLite + JSON state is stored | `storage/` |
187+
| `NEURALCACHE_STORAGE_PERSISTENCE_ENABLED` | Disable to keep narrative + pheromones in-memory only | `true` |
188+
| `NEURALCACHE_STORAGE_RETENTION_DAYS` | Days before old state is purged on boot (supports SQLite + JSON) | _unset_ |
137189
| `NEURALCACHE_GATING_MODE` | Cognitive gate mode (`off`, `auto`, `on`) | `auto` |
138190
| `NEURALCACHE_GATING_THRESHOLD` | Uncertainty threshold for trimming | `0.45` |
139191
| `NEURALCACHE_GATING_MIN_CANDIDATES` | Lower bound for rerank candidates | `8` |
@@ -142,7 +194,11 @@ See [`examples/quickstart.py`](examples/quickstart.py) for an end-to-end script.
142194

143195
Adjust everything via `.env`, environment variables, or direct `Settings(...)` instantiation.
144196

145-
Persistence happens automatically using SQLite (or JSON fallback) so narrative and pheromone stores survive restarts. Point `NEURALCACHE_STORAGE_DIR` at shared storage for multi-worker deployments, or import `SQLiteState` directly if you need to wire the persistence layer into an existing app container.
197+
Persistence happens automatically using SQLite (or JSON fallback) so narrative and pheromone stores survive restarts. Point `NEURALCACHE_STORAGE_DIR` at shared storage for multi-worker deployments, or import `SQLiteState` directly if you need to wire the persistence layer into an existing app container. Under the hood the SQLite state:
198+
199+
- enables **WAL mode** with `synchronous=NORMAL` so multiple workers can read while a writer appends.
200+
- tracks a `metadata` row with the current schema version (`SQLiteState.schema_version()`), raising if a newer schema is encountered so upgrades can run explicit migrations before boot.
201+
- stores pheromone exposures and timestamps so retention/evaporation policies can prune long-lived records.
146202

147203
---
148204

‎SECURITY.md‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Security Policy
2+
3+
## Supported Versions
4+
5+
We support the most recent minor release line of `neuralcache`. Security fixes are
6+
backported to the latest published version on PyPI. Older minors may receive fixes
7+
on a best-effort basis only if they are no more than one release behind.
8+
9+
| Version | Supported |
10+
| ------- | --------- |
11+
| 0.3.x | ✅ |
12+
| < 0.3 | ⚠️ (upgrade recommended) |
13+
14+
## Reporting a Vulnerability
15+
16+
If you discover a security issue, please email **security@carnotengine.com** with the
17+
following details:
18+
19+
- A clear description of the vulnerability.
20+
- Steps or proof-of-concept required to reproduce the issue.
21+
- The impact you believe the vulnerability has.
22+
- Any suggested fixes or mitigations.
23+
24+
We aim to acknowledge new reports within **3 business days** and will keep you updated
25+
on progress. Please do not open public GitHub issues for potential vulnerabilities.
26+
27+
## Handling Sensitive Data
28+
29+
NeuralCache can store reranking telemetry in SQLite. When deploying to production:
30+
31+
- Place the SQLite database on encrypted storage.
32+
- Rotate API tokens stored in environment variables regularly.
33+
- Run the API behind TLS (e.g., via a reverse proxy such as Nginx or Caddy).
34+
- Set `NEURALCACHE_API_TOKENS` to enforce bearer-token authentication.
35+
36+
## Dependency Management
37+
38+
- The project uses constrained versions for FastAPI and Uvicorn to avoid known security
39+
advisories. Update pins only after verifying advisories in the
40+
[Python Packaging Advisory Database](https://github.com/pypa/advisory-database).
41+
- The CI workflow runs [`pip-audit`](https://github.com/pypa/pip-audit) on every pull
42+
request and push to `main` to detect vulnerable dependencies early.
43+
- Use Dependabot updates to stay ahead of transitive dependency advisories.
44+
45+
## Secure Development Checklist
46+
47+
- Run `ruff`, `mypy`, and `pytest` locally before sending a pull request.
48+
- Avoid storing secrets in the repository or sample configuration files.
49+
- Keep container builds based on the published Dockerfile up to date with the
50+
latest security patches from the base image.
51+
52+
## Coordinated Disclosure
53+
54+
We prefer coordinated disclosure. After we release a fix, we'll work with you
55+
on appropriate public communication and attribution if desired. Thank you for
56+
helping us keep NeuralCache safe for everyone.

‎pyproject.toml‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ classifiers = [
2020
]
2121

2222
dependencies = [
23-
"fastapi>=0.110,<1.0",
24-
"uvicorn[standard]>=0.24",
23+
"fastapi>=0.111,<0.114",
24+
"uvicorn[standard]>=0.24,<0.30",
2525
"numpy>=1.26",
2626
"pydantic>=2.6",
2727
"pydantic-settings>=2.2",
@@ -30,9 +30,6 @@ dependencies = [
3030
"orjson>=3.9",
3131
"python-dateutil>=2.9",
3232
"rich>=13.7",
33-
# optional adapters (safe to import conditionally)
34-
"langchain-core>=0.2; python_version >= '3.11'",
35-
"llama-index-core>=0.10; python_version >= '3.11'",
3633
]
3734

3835
[project.optional-dependencies]
@@ -43,6 +40,10 @@ embeddings = [
4340
"sentence-transformers>=2.6.0"
4441
]
4542
ops = ["prometheus-client>=0.20", "requests>=2.32"]
43+
adapters = [
44+
"langchain-core>=0.2",
45+
"llama-index-core>=0.10",
46+
]
4647

4748
[project.scripts]
4849
neuralcache = "neuralcache.cli:app"

‎src/neuralcache/config.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ class Settings(BaseSettings):
5454
storage_backend: str = "sqlite"
5555
storage_dir: str = "storage"
5656
storage_db_name: str = "neuralcache.db"
57+
storage_persistence_enabled: bool = True
58+
storage_retention_days: float | None = None
5759
narrative_store_path: str = "narrative.json"
5860
pheromone_store_path: str = "pheromones.json"
5961

‎src/neuralcache/narrative.py‎

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import json
44
import pathlib
5+
import time
56
from contextlib import suppress
67

78
import numpy as np
@@ -28,14 +29,19 @@ def __init__(
2829
self.path = (base_path / path).as_posix()
2930
self._sqlite = sqlite_state
3031
self.v = np.zeros((dim,), dtype=np.float32)
32+
self._updated_ts = 0.0
3133
self._load()
3234

3335
def _load(self) -> None:
36+
if self.backend == "memory":
37+
self._updated_ts = 0.0
38+
return
3439
if self.backend == "sqlite" and self._sqlite is not None:
3540
with suppress(Exception):
36-
stored= self._sqlite.load_narrative()
41+
stored, updated_ts= self._sqlite.load_narrative_record()
3742
if stored is not None and stored.size == self.v.size:
3843
self.v = stored.astype(np.float32)
44+
self._updated_ts = float(updated_ts or 0.0)
3945
return
4046

4147
path = pathlib.Path(self.path)
@@ -48,8 +54,13 @@ def _load(self) -> None:
4854
arr = np.array(data.get("v", []), dtype=np.float32)
4955
if arr.size == self.v.size:
5056
self.v = arr
57+
self._updated_ts = float(data.get("updated_ts", 0.0))
5158

5259
def _save(self) -> None:
60+
now = time.time()
61+
self._updated_ts = now
62+
if self.backend == "memory":
63+
return
5364
if self.backend == "sqlite" and self._sqlite is not None:
5465
with suppress(Exception):
5566
self._sqlite.save_narrative(self.v)
@@ -58,7 +69,7 @@ def _save(self) -> None:
5869
path = pathlib.Path(self.path)
5970
path.parent.mkdir(parents=True, exist_ok=True)
6071
with suppress(Exception), path.open("w", encoding="utf-8") as handle:
61-
json.dump({"v": self.v.tolist()}, handle)
72+
json.dump({"v": self.v.tolist(), "updated_ts": now}, handle)
6273
with suppress(Exception):
6374
path.chmod(0o600)
6475

@@ -82,3 +93,22 @@ def coherence(self, doc_embeddings: np.ndarray) -> np.ndarray:
8293
docs_norm = safe_normalize(doc_embeddings)
8394
sims = (v @ docs_norm.T).reshape(-1)
8495
return sims.astype(np.float32)
96+
97+
def purge_if_stale(self, retention_seconds: float) -> None:
98+
if retention_seconds <= 0:
99+
return
100+
cutoff = time.time() - retention_seconds
101+
if self._updated_ts == 0 or self._updated_ts >= cutoff:
102+
return
103+
self.v = np.zeros_like(self.v, dtype=np.float32)
104+
self._updated_ts = 0.0
105+
if self.backend == "sqlite" and self._sqlite is not None:
106+
with suppress(Exception):
107+
self._sqlite.clear_narrative()
108+
elif self.backend == "json":
109+
path = pathlib.Path(self.path)
110+
with suppress(FileNotFoundError):
111+
path.unlink()
112+
113+
def refresh(self) -> None:
114+
self._load()

‎src/neuralcache/pheromone.py‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ def __init__(
3131
self._load()
3232

3333
def _load(self) -> None:
34+
if self.backend == "memory":
35+
return
3436
path = pathlib.Path(self.path)
3537
if not path.exists():
3638
return
@@ -43,6 +45,8 @@ def _load(self) -> None:
4345
def _save(self) -> None:
4446
if self.backend == "sqlite" and self._sqlite is not None:
4547
return
48+
if self.backend == "memory":
49+
return
4650

4751
path = pathlib.Path(self.path)
4852
path.parent.mkdir(parents=True, exist_ok=True)
@@ -133,3 +137,20 @@ def record_exposure(self, ids: list[str]) -> None:
133137
self.data[doc_id] = rec
134138
if self.backend == "json":
135139
self._save()
140+
141+
def purge_older_than(self, retention_seconds: float) -> None:
142+
if retention_seconds <= 0:
143+
return
144+
if self.backend == "sqlite" and self._sqlite is not None:
145+
self._sqlite.purge_older_than(retention_seconds)
146+
return
147+
cutoff = time.time() - retention_seconds
148+
stale_keys = [
149+
doc_id
150+
for doc_id, rec in self.data.items()
151+
if float(rec.get("t", 0.0)) < cutoff
152+
]
153+
for key in stale_keys:
154+
self.data.pop(key, None)
155+
if self.backend == "json":
156+
self._save()

‎src/neuralcache/rerank.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,19 @@ def __init__(self, settings: Settings | None = None):
5656
)
5757
self._cr_index: CRIndex | None = None
5858
storage_backend = (self.settings.storage_backend or "sqlite").lower()
59+
if not self.settings.storage_persistence_enabled:
60+
storage_backend = "memory"
5961
storage_dir = Path(self.settings.storage_dir or ".")
6062
storage_dir.mkdir(parents=True, exist_ok=True)
6163
sqlite_state: SQLiteState | None = None
64+
retention_seconds: float | None = None
65+
if self.settings.storage_retention_days is not None:
66+
retention_seconds = max(0.0, float(self.settings.storage_retention_days) * 86400.0)
6267
if storage_backend == "sqlite":
6368
db_path = storage_dir / self.settings.storage_db_name
6469
sqlite_state = SQLiteState(path=str(db_path))
70+
if retention_seconds:
71+
sqlite_state.purge_older_than(retention_seconds)
6572
self._sqlite_state = sqlite_state
6673
self.narr = NarrativeTracker(
6774
dim=self.settings.narrative_dim,
@@ -80,6 +87,9 @@ def __init__(self, settings: Settings | None = None):
8087
storage_dir=str(storage_dir),
8188
sqlite_state=sqlite_state,
8289
)
90+
if retention_seconds:
91+
self.narr.purge_if_stale(retention_seconds)
92+
self.pher.purge_older_than(retention_seconds)
8393

8494
def _ensure_cr_loaded(self) -> CRIndex | None:
8595
if not self.settings.cr.on:

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2026 Movatter.jp