frankenstein / hands /hands_server.py
SNAPKITTYWEST's picture
Add Frankenstein source: Rust Tokio BRAIN/HANDS/LEGS pipeline, gates, trust deed
3d2a871 verified
Raw
History Blame Contribute Delete
16.8 kB
"""
FRANKENSTEIN HANDS β€” Sovereign Knowledge Retrieval Sidecar
Port 5433
Pipeline:
query
β†’ spaCy + NLTK (NLP parse: entities, POS, intent, key concepts)
β†’ FAISS (vector similarity β†’ top-K corpus chunks)
β†’ Neo4j (graph traversal β†’ expand related nodes)
β†’ RDF / rdflib (semantic triples for extracted entities)
β†’ NetworkX (rank by PageRank / betweenness centrality)
β†’ tagged context β†’ Frankenstein Brain
Each layer degrades gracefully if its backend is offline.
Every retrieval is WORM-tagged with source metadata.
SnapKitty Collective 2026 Β· Apache 2.0 Β· Evidence or Silence
"""
import hashlib, json, os, time
from datetime import datetime
from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel
# ── Optional heavy deps β€” degrade gracefully ──────────────────────────────────
try:
import spacy
nlp = spacy.load("en_core_web_sm")
SPACY_OK = True
except Exception:
SPACY_OK = False
try:
import nltk
from nltk.corpus import wordnet, stopwords
from nltk.tokenize import word_tokenize
nltk.download("punkt", quiet=True)
nltk.download("wordnet", quiet=True)
nltk.download("stopwords", quiet=True)
nltk.download("averaged_perceptron_tagger", quiet=True)
NLTK_OK = True
STOPS = set(stopwords.words("english"))
except Exception:
NLTK_OK = False
STOPS = set()
try:
import faiss
import numpy as np
FAISS_OK = True
except Exception:
FAISS_OK = False
try:
from neo4j import GraphDatabase
NEO4J_URI = os.getenv("NEO4J_URI", "bolt://localhost:7687")
NEO4J_USER = os.getenv("NEO4J_USER", "neo4j")
NEO4J_PASS = os.getenv("NEO4J_PASS", "sovereign")
neo4j_driver = GraphDatabase.driver(NEO4J_URI, auth=(NEO4J_USER, NEO4J_PASS))
NEO4J_OK = True
except Exception:
NEO4J_OK = False
try:
from rdflib import Graph as RDFGraph, URIRef, Literal, Namespace
from rdflib.namespace import RDF, RDFS, OWL
rdf_g = RDFGraph()
SKC = Namespace("https://snapkitty.io/ontology#")
rdf_g.bind("skc", SKC)
RDF_OK = True
except Exception:
RDF_OK = False
try:
import networkx as nx
NETX_OK = True
except Exception:
NETX_OK = False
try:
from sentence_transformers import SentenceTransformer
embed_model = SentenceTransformer("all-MiniLM-L6-v2")
EMBED_OK = True
except Exception:
EMBED_OK = False
# ── In-memory FAISS index (loaded from corpus on startup) ─────────────────────
faiss_index = None
faiss_texts: list[str] = []
faiss_meta: list[dict] = []
# ── NetworkX knowledge graph (built from Neo4j + RDF on startup) ──────────────
nx_graph = nx.DiGraph() if NETX_OK else None
# ── WORM chain ────────────────────────────────────────────────────────────────
worm_prev = "HANDS_GENESIS"
worm_count = 0
def worm_seal(event: str) -> str:
global worm_prev, worm_count
msg = f"{worm_prev}|{event}|{int(time.time()*1000)}"
h = hashlib.sha256(msg.encode()).hexdigest()
worm_prev = h
worm_count += 1
return h[:16]
# ── FastAPI app ───────────────────────────────────────────────────────────────
app = FastAPI(title="FRANKENSTEIN HANDS", version="1.0.0")
class RetrieveRequest(BaseModel):
query: str
k: int = 5
class IndexRequest(BaseModel):
texts: list[str]
metas: list[dict] = []
class NeoIngestRequest(BaseModel):
nodes: list[dict] # [{"id": "...", "label": "...", "props": {...}}]
edges: list[dict] # [{"from": "...", "to": "...", "rel": "..."}]
# ── NLP PARSE β€” spaCy + NLTK ──────────────────────────────────────────────────
def nlp_parse(query: str) -> dict:
result = {"entities": [], "concepts": [], "pos_tags": [], "wordnet": [], "intent": "query"}
if SPACY_OK:
doc = nlp(query)
result["entities"] = [
{"text": ent.text, "label": ent.label_, "start": ent.start_char, "end": ent.end_char}
for ent in doc.ents
]
result["concepts"] = [
tok.lemma_ for tok in doc
if tok.pos_ in ("NOUN", "VERB", "PROPN") and not tok.is_stop and len(tok.text) > 2
]
result["pos_tags"] = [{"text": tok.text, "pos": tok.pos_} for tok in doc]
# Detect intent from dependency root
for tok in doc:
if tok.dep_ == "ROOT":
if tok.pos_ == "VERB":
result["intent"] = tok.lemma_
break
if NLTK_OK:
tokens = word_tokenize(query)
content = [t for t in tokens if t.lower() not in STOPS and t.isalpha()]
result["nltk_tokens"] = content
# WordNet synonyms for key concepts
wn_hits = []
for word in content[:4]:
syns = wordnet.synsets(word)
if syns:
wn_hits.append({
"word": word,
"definition": syns[0].definition(),
"examples": syns[0].examples()[:1],
"hypernyms": [h.name() for h in syns[0].hypernyms()[:2]],
})
result["wordnet"] = wn_hits
return result
# ── FAISS RETRIEVE ────────────────────────────────────────────────────────────
def faiss_retrieve(query: str, k: int) -> list[dict]:
if not FAISS_OK or not EMBED_OK or faiss_index is None:
return [{"text": "FAISS_OFFLINE β€” no index loaded", "score": 0.0, "meta": {}}]
vec = embed_model.encode([query], normalize_embeddings=True).astype("float32")
scores, idxs = faiss_index.search(vec, min(k, len(faiss_texts)))
return [
{
"text": faiss_texts[i],
"score": float(scores[0][n]),
"meta": faiss_meta[i] if i < len(faiss_meta) else {},
}
for n, i in enumerate(idxs[0]) if i >= 0
]
# ── NEO4J EXPAND ─────────────────────────────────────────────────────────────
def neo4j_expand(concepts: list[str], k: int) -> list[dict]:
if not NEO4J_OK or not concepts:
return [{"node": "NEO4J_OFFLINE", "rels": []}]
try:
with neo4j_driver.session() as s:
results = []
for concept in concepts[:3]:
q = (
"MATCH (n)-[r]->(m) "
"WHERE toLower(n.name) CONTAINS toLower($c) "
"RETURN n.name AS src, type(r) AS rel, m.name AS tgt, m.description AS desc "
"LIMIT $k"
)
rows = s.run(q, c=concept, k=k).data()
results.extend([{
"node": r["src"], "rel": r["rel"],
"target": r["tgt"], "desc": r.get("desc","")
} for r in rows])
return results or [{"node": "NO_MATCH", "concept": concepts}]
except Exception as e:
return [{"node": f"NEO4J_ERR: {e}"}]
# ── RDF TRIPLES ───────────────────────────────────────────────────────────────
def rdf_triples(entities: list[str]) -> list[dict]:
if not RDF_OK or not entities:
return []
results = []
for ent in entities[:4]:
subj = URIRef(f"https://snapkitty.io/ontology#{ent.replace(' ','_')}")
for _, pred, obj in rdf_g.triples((subj, None, None)):
results.append({"subject": ent, "predicate": str(pred), "object": str(obj)})
return results or [{"rdf": "NO_TRIPLES_YET β€” load an ontology via /ingest/rdf"}]
# ── NETWORKX RANK ─────────────────────────────────────────────────────────────
def netx_rank(faiss_hits: list[dict], neo4j_hits: list[dict]) -> list[dict]:
if not NETX_OK:
return faiss_hits
# Build mini graph from retrieved hits
G = nx.DiGraph()
for h in faiss_hits:
G.add_node(h["text"][:64], score=h["score"], type="corpus")
for h in neo4j_hits:
src = h.get("node","?")
tgt = h.get("target","?")
if src and tgt and src != "NEO4J_OFFLINE":
G.add_edge(src, tgt, rel=h.get("rel","RELATED"))
if len(G.nodes) == 0:
return faiss_hits
try:
pr = nx.pagerank(G, alpha=0.85)
except Exception:
pr = {}
ranked = sorted(faiss_hits, key=lambda h: pr.get(h["text"][:64], h["score"]), reverse=True)
return ranked
# ── Task pool helpers (run sync functions in thread pool) ─────────────────────
import asyncio
from concurrent.futures import ThreadPoolExecutor
_pool = ThreadPoolExecutor(max_workers=8, thread_name_prefix="hands")
async def _run(fn, *args):
loop = asyncio.get_running_loop()
return await loop.run_in_executor(_pool, fn, *args)
async def _safe(label: str, coro):
"""Run a coroutine with error isolation β€” never lets one layer kill the rest."""
try:
return label, await coro
except asyncio.TimeoutError:
return label, {"error": f"{label}_TIMEOUT"}
except Exception as e:
return label, {"error": f"{label}_ERR: {type(e).__name__}: {e}"}
# ── /retrieve β€” main endpoint (TaskGroup: all layers fire concurrently) ────────
@app.post("/retrieve")
async def retrieve(req: RetrieveRequest):
t0 = time.time()
# Phase 1: NLP parse (must complete before graph layers need concepts)
parse = await _run(nlp_parse, req.query)
concepts = parse.get("concepts", [])
ent_texts = [e["text"] for e in parse.get("entities", [])]
# Phase 2: Fire FAISS + Neo4j + RDF concurrently via TaskGroup
# Each layer is isolated β€” failure in one does NOT cancel others
layer_results = {}
async def _faiss():
return await asyncio.wait_for(_run(faiss_retrieve, req.query, req.k), timeout=5.0)
async def _neo4j():
return await asyncio.wait_for(_run(neo4j_expand, concepts, req.k), timeout=8.0)
async def _rdf():
return await asyncio.wait_for(_run(rdf_triples, ent_texts), timeout=3.0)
# asyncio.gather with return_exceptions=True β†’ error in one doesn't cancel others
faiss_r, neo4j_r, rdf_r = await asyncio.gather(
_faiss(), _neo4j(), _rdf(),
return_exceptions=True
)
# Normalize exceptions into error dicts
def safe_result(r, default):
if isinstance(r, Exception):
return [{"error": f"{type(r).__name__}: {r}"}]
return r if r else default
chunks = safe_result(faiss_r, [{"text": "FAISS_OFFLINE", "score": 0, "meta": {}}])
neo4j = safe_result(neo4j_r, [{"node": "NEO4J_OFFLINE"}])
triples = safe_result(rdf_r, [])
# Phase 3: NetworkX rank (uses outputs from phase 2)
ranked = await _run(netx_rank, chunks, neo4j)
# WORM seal
seal = worm_seal(f"RETRIEVE|{req.query[:32]}")
# Build context string for Brain (formatted for easy consumption)
context_lines = []
for i, r in enumerate(ranked[:req.k]):
text = r.get("text","")
if text and not text.startswith("FAISS_OFFLINE"):
context_lines.append(f"[{i+1}] (score={r.get('score',0):.3f}) {text}")
for g in neo4j[:3]:
if "node" in g and g["node"] not in ("NEO4J_OFFLINE","NO_MATCH"):
context_lines.append(f"[GRAPH] {g.get('node','')} --{g.get('rel','')}β†’ {g.get('target','')}: {g.get('desc','')}")
for t in triples[:2]:
context_lines.append(f"[RDF] {t.get('subject','')} {t.get('predicate','').split('#')[-1]} {t.get('object','')}")
results = [{"text": r["text"], "score": r.get("score",0), "meta": r.get("meta",{})} for r in ranked]
return {
"query": req.query,
"results": results,
"context": "\n".join(context_lines) if context_lines else "NO_CONTEXT",
"nlp": parse,
"graph": neo4j[:5],
"rdf": triples[:5],
"layers": {
"spacy": SPACY_OK,
"nltk": NLTK_OK,
"faiss": FAISS_OK and faiss_index is not None,
"neo4j": NEO4J_OK,
"rdf": RDF_OK,
"networkx": NETX_OK,
"embed": EMBED_OK,
},
"worm": seal,
"ms": round((time.time() - t0) * 1000, 1),
}
# ── /index β€” load corpus into FAISS ──────────────────────────────────────────
@app.post("/index")
async def index_corpus(req: IndexRequest):
global faiss_index, faiss_texts, faiss_meta
if not FAISS_OK or not EMBED_OK:
return {"error": "FAISS or sentence-transformers not installed"}
vecs = embed_model.encode(req.texts, normalize_embeddings=True).astype("float32")
dim = vecs.shape[1]
faiss_index = faiss.IndexFlatIP(dim) # Inner product = cosine (normalized vecs)
faiss_index.add(vecs)
faiss_texts = req.texts
faiss_meta = req.metas or [{} for _ in req.texts]
seal = worm_seal(f"INDEX|{len(req.texts)}_docs")
return {"indexed": len(req.texts), "dim": dim, "worm": seal}
# ── /ingest/rdf β€” load RDF triples ───────────────────────────────────────────
@app.post("/ingest/rdf")
async def ingest_rdf(payload: dict):
if not RDF_OK:
return {"error": "rdflib not installed"}
triples_added = 0
for triple in payload.get("triples", []):
s = URIRef(triple["subject"])
p = URIRef(triple["predicate"])
o = Literal(triple["object"]) if triple.get("literal") else URIRef(triple["object"])
rdf_g.add((s, p, o))
triples_added += 1
return {"added": triples_added, "total": len(rdf_g)}
# ── /ingest/graph β€” load into Neo4j + NetworkX ────────────────────────────────
@app.post("/ingest/graph")
async def ingest_graph(req: NeoIngestRequest):
results = {"neo4j": "offline", "networkx": 0}
if NEO4J_OK:
try:
with neo4j_driver.session() as s:
for node in req.nodes:
s.run(
"MERGE (n:Entity {id: $id}) SET n.name=$name, n += $props",
id=node["id"], name=node.get("label",""), props=node.get("props",{})
)
for edge in req.edges:
s.run(
f"MATCH (a:Entity {{id:$f}}),(b:Entity {{id:$t}}) MERGE (a)-[:{edge['rel']}]->(b)",
f=edge["from"], t=edge["to"]
)
results["neo4j"] = f"ok β€” {len(req.nodes)} nodes, {len(req.edges)} edges"
except Exception as e:
results["neo4j"] = f"error: {e}"
if NETX_OK:
for node in req.nodes:
nx_graph.add_node(node["id"], **node.get("props",{}))
for edge in req.edges:
nx_graph.add_edge(edge["from"], edge["to"], rel=edge["rel"])
results["networkx"] = nx_graph.number_of_nodes()
return results
# ── /health ────────────────────────────────────────────────────────────────────
@app.get("/health")
async def health():
return {
"ok": True,
"service": "frankenstein-hands",
"version": "1.0.0",
"worm_seals": worm_count,
"layers": {
"spacy": SPACY_OK,
"nltk": NLTK_OK,
"faiss": FAISS_OK,
"neo4j": NEO4J_OK,
"rdf": RDF_OK,
"networkx": NETX_OK,
"embed": EMBED_OK,
"index_loaded": faiss_index is not None,
}
}
if __name__ == "__main__":
import uvicorn
print("⬑ FRANKENSTEIN HANDS β†’ http://localhost:5433")
print(" FAISS + Neo4j + RDF + NetworkX + spaCy + NLTK")
uvicorn.run(app, host="0.0.0.0", port=5433)