cypher-v12-finalized / modules /cypher_semantic_cache.py
jescy525's picture
Upload folder using huggingface_hub
076a67c verified
Raw
History Blame Contribute Delete
10.8 kB
"""CYPHER V12 M25 — Semantic Response Cache.
Cache responses keyed by semantic similarity of prompts (not exact match).
Uses sentence-transformers (all-MiniLM-L6-v2 CPU OK) for embeddings + FAISS
or in-memory cosine similarity for retrieval.
Returns cached response if similarity > threshold AND cache entry is fresh.
Reduces compute by ~70% on repeated/paraphrased queries.
Backed by SQLite for persistence + TTL eviction.
"""
from __future__ import annotations
import hashlib
import json
import logging
import sqlite3
import time
from pathlib import Path
from typing import Any
import numpy as np
logger = logging.getLogger(__name__)
class SemanticResponseCache:
"""Persistent semantic cache for /chat responses.
Schema:
cache(id, prompt_hash, prompt_text, response, category, ts, ttl, hits, embedding_blob)
"""
def __init__(
self,
db_path: str = "/workspace/CYPHER_V12/cache/semantic_cache.sqlite",
embedding_model: str = "all-MiniLM-L6-v2",
similarity_threshold: float = 0.90,
max_entries: int = 10000,
default_ttl_sec: int = 86400, # 24h
):
self.db_path = Path(db_path)
self.db_path.parent.mkdir(parents=True, exist_ok=True)
self.similarity_threshold = similarity_threshold
self.max_entries = max_entries
self.default_ttl_sec = default_ttl_sec
self._embed_dim: int | None = None
self._embed_model_name = embedding_model
self._embedder = None # lazy
self._conn = sqlite3.connect(str(self.db_path), check_same_thread=False)
self._init_schema()
# In-memory index for fast cosine
self._mem_ids: list[int] = []
self._mem_embeds: np.ndarray | None = None
self._load_mem_index()
def _init_schema(self) -> None:
c = self._conn.cursor()
c.execute("""
CREATE TABLE IF NOT EXISTS cache (
id INTEGER PRIMARY KEY AUTOINCREMENT,
prompt_hash TEXT UNIQUE,
prompt_text TEXT,
response TEXT,
category TEXT,
ts INTEGER,
ttl INTEGER,
hits INTEGER DEFAULT 0,
embedding BLOB
)
""")
c.execute("CREATE INDEX IF NOT EXISTS idx_cache_ts ON cache(ts)")
c.execute("CREATE INDEX IF NOT EXISTS idx_cache_cat ON cache(category)")
self._conn.commit()
def _get_embedder(self):
if self._embedder is None:
try:
from sentence_transformers import SentenceTransformer
self._embedder = SentenceTransformer(self._embed_model_name, device="cpu")
test_emb = self._embedder.encode(["test"], normalize_embeddings=True)
self._embed_dim = test_emb.shape[1]
except Exception as e:
logger.error(f"Embedder init fail: {e}")
self._embedder = None
return self._embedder
def _embed(self, text: str) -> np.ndarray | None:
emb = self._get_embedder()
if emb is None:
return None
try:
v = emb.encode([text], normalize_embeddings=True)
return np.array(v[0], dtype=np.float32)
except Exception as e:
logger.warning(f"Embed fail: {e}")
return None
def _load_mem_index(self) -> None:
c = self._conn.cursor()
c.execute("SELECT id, embedding FROM cache WHERE embedding IS NOT NULL ORDER BY ts DESC LIMIT ?",
(self.max_entries,))
rows = c.fetchall()
if not rows:
self._mem_ids = []
self._mem_embeds = None
return
ids: list[int] = []
embeds: list[np.ndarray] = []
for row in rows:
try:
vec = np.frombuffer(row[1], dtype=np.float32)
ids.append(int(row[0]))
embeds.append(vec)
except Exception:
continue
self._mem_ids = ids
if embeds:
self._mem_embeds = np.vstack(embeds)
self._embed_dim = self._mem_embeds.shape[1]
else:
self._mem_embeds = None
logger.info(f"Semantic cache loaded {len(self._mem_ids)} entries")
@staticmethod
def _hash(text: str) -> str:
return hashlib.sha256(text.encode("utf-8")).hexdigest()[:32]
def lookup(self, prompt: str, category: str | None = None) -> dict | None:
"""Returns cached entry if found above threshold, else None."""
h = self._hash(prompt)
# Exact match first
c = self._conn.cursor()
c.execute("SELECT id, response, category, ts, ttl, hits FROM cache WHERE prompt_hash = ?", (h,))
row = c.fetchone()
if row:
cid, resp, cat, ts, ttl, hits = row
if time.time() - ts < ttl:
c.execute("UPDATE cache SET hits = hits + 1 WHERE id = ?", (cid,))
self._conn.commit()
return {"response": resp, "category": cat, "match_type": "exact",
"similarity": 1.0, "age_sec": int(time.time() - ts), "hits": hits + 1}
# Semantic match
if self._mem_embeds is None or len(self._mem_ids) == 0:
return None
qvec = self._embed(prompt)
if qvec is None:
return None
# Cosine sim (embeddings already normalized)
sims = self._mem_embeds @ qvec
best_idx = int(np.argmax(sims))
best_sim = float(sims[best_idx])
if best_sim < self.similarity_threshold:
return None
cid = self._mem_ids[best_idx]
c.execute("SELECT response, category, ts, ttl, hits FROM cache WHERE id = ?", (cid,))
row = c.fetchone()
if not row:
return None
resp, cat, ts, ttl, hits = row
if time.time() - ts >= ttl:
return None
if category and cat and cat != category:
return None # Don't reuse across categories
c.execute("UPDATE cache SET hits = hits + 1 WHERE id = ?", (cid,))
self._conn.commit()
return {"response": resp, "category": cat, "match_type": "semantic",
"similarity": round(best_sim, 4), "age_sec": int(time.time() - ts), "hits": hits + 1}
def store(
self,
prompt: str,
response: str,
category: str | None = None,
ttl_sec: int | None = None,
) -> bool:
h = self._hash(prompt)
emb = self._embed(prompt)
emb_blob = emb.tobytes() if emb is not None else None
ts = int(time.time())
ttl = ttl_sec if ttl_sec is not None else self.default_ttl_sec
c = self._conn.cursor()
try:
c.execute("""
INSERT OR REPLACE INTO cache
(prompt_hash, prompt_text, response, category, ts, ttl, hits, embedding)
VALUES (?, ?, ?, ?, ?, ?, 0, ?)
""", (h, prompt, response, category, ts, ttl, emb_blob))
self._conn.commit()
except sqlite3.Error as e:
logger.error(f"Cache insert fail: {e}")
return False
# Rebuild in-memory index occasionally (every 100 inserts)
if len(self._mem_ids) % 100 == 0:
self._load_mem_index()
return True
def evict_stale(self) -> int:
"""Remove entries past TTL or above max_entries."""
now = int(time.time())
c = self._conn.cursor()
c.execute("DELETE FROM cache WHERE ts + ttl < ?", (now,))
n_ttl = c.rowcount
# Over max_entries: drop oldest
c.execute("SELECT COUNT(*) FROM cache")
total = c.fetchone()[0]
n_over = 0
if total > self.max_entries:
n_over = total - self.max_entries
c.execute("DELETE FROM cache WHERE id IN (SELECT id FROM cache ORDER BY ts ASC LIMIT ?)", (n_over,))
self._conn.commit()
self._load_mem_index()
return n_ttl + n_over
def stats(self) -> dict:
c = self._conn.cursor()
c.execute("SELECT COUNT(*), SUM(hits), AVG(hits) FROM cache")
total, sum_hits, avg_hits = c.fetchone()
c.execute("SELECT category, COUNT(*) FROM cache GROUP BY category")
per_cat = dict(c.fetchall())
return {
"total_entries": total or 0,
"total_hits": sum_hits or 0,
"avg_hits_per_entry": float(avg_hits or 0),
"per_category": per_cat,
"memory_index_size": len(self._mem_ids),
"embedding_dim": self._embed_dim,
"threshold": self.similarity_threshold,
}
def close(self) -> None:
try:
self._conn.close()
except Exception:
pass
__all__ = ["SemanticResponseCache"]
if __name__ == "__main__":
import shutil
logging.basicConfig(level=logging.INFO)
print("=== M25 cypher_semantic_cache SMOKE ===")
test_db = "/tmp/smoke_semantic_cache.sqlite"
if Path(test_db).exists():
Path(test_db).unlink()
cache = SemanticResponseCache(
db_path=test_db,
similarity_threshold=0.85,
default_ttl_sec=3600,
)
# Store some Q&A pairs
cache.store("Who are you?", "I am CYPHER, the defensive cybersecurity ASI.", "IDENTITY")
cache.store("What is CVE-2021-44228?", "CVE-2021-44228 (Log4Shell) is a critical RCE in Apache Log4j2 CVSS 10.0.", "CYBERSEC")
cache.store("Explain Order Block", "An Order Block is an institutional candle that left imbalance.", "TRADING")
print(f"Stored 3 entries")
# Exact match
hit = cache.lookup("Who are you?", category="IDENTITY")
print(f"\nExact lookup 'Who are you?': match_type={hit['match_type']} sim={hit['similarity']}")
print(f" Response: {hit['response'][:80]}")
# Semantic match
hit2 = cache.lookup("Can you tell me who you are?", category="IDENTITY")
if hit2:
print(f"\nSemantic lookup 'Can you tell me who you are?': match_type={hit2['match_type']} sim={hit2['similarity']}")
print(f" Response: {hit2['response'][:80]}")
else:
print(f"\nNo semantic match (try lowering threshold)")
# Semantic match for paraphrase
hit3 = cache.lookup("Tell me about Log4Shell vulnerability", category="CYBERSEC")
if hit3:
print(f"\nSemantic lookup 'Log4Shell': match_type={hit3['match_type']} sim={hit3['similarity']}")
# Miss
hit4 = cache.lookup("What is quantum computing?", category="CONV")
print(f"\nMiss 'quantum computing': {hit4}")
# Cross-category should not match
hit5 = cache.lookup("Who are you?", category="TRADING")
print(f"\nCross-cat 'Who are you?' as TRADING: {hit5}")
# Stats
print(f"\nStats: {cache.stats()}")
cache.close()
print("=== SMOKE PASS ===")