A full RAG system.
Clone the examples repo, install, and run from inside the example's folder.
$ git clone https://github.com/Cosmonapse/cosmonapse-examples $ pip install 'cosmonapse[receptor]' httpx python-dotenv # a Hugging Face token with read scope, in cosmonapse-examples/.env $ echo "HF_TOKEN=hf_xxxxxxxxxxxxxxxx" >> cosmonapse-examples/.env $ cd cosmonapse-examples/11-rag $ python brain.py "What is a Dendrite and what role does it play?" $ python brain.py stats $ python brain.py # REPL + the HTTP API on :8000 - ask twice to hit the cache $ curl -s localhost:8000/ask -H 'content-type: application/json' -d '{"question": "What is a Dendrite?"}' $ curl -s localhost:8000/ingest -H 'content-type: application/json' -d '{"doc_id": "notes", "text": "..."}' $ curl -s localhost:8000/stats
indexed cosmonapse-core 2 chunks indexed memory-and-pathways 2 chunks indexed routing-and-bidding 2 chunks A Dendrite is the Synapse-side participant that hosts Axons ... [cosmonapse-core#0] sources: cosmonapse-core#0 (1.577), memory-and-pathways#1 (1.4569), ...
The answer text varies; the pipeline shape does not.
One component per file, one Dendrite per node.
The skeleton cosmo init scaffolds. Modules under neurons/, engram/, effector/ and receptors/ declare behaviour; brain.py owns deployment - which node hosts what.
11-rag/ config.py models, TOP_K / FETCH_K / MIN_SCORE, cache_key() embeddings.py HF embedding client + chunker engram/vector_engram.py VectorEngram - cosine, over the Engram ABC engram/keyword_engram.py KeywordEngram - BM25 engram/indexes.py VECTORS, KEYWORDS, CACHE neurons/ ingester, retriever, reranker, generator helpers.py ask_pipeline(), ingest_samples(), stats() receptors/terminal.py ask, stats receptors/api.py POST /ingest, POST /ask, GET /stats brain.py a node per Engram, per Neuron, per interface
Two Engrams, one protocol.
VectorEngram and KeywordEngram implement the Engram ABC, so RECALL and IMPRINT carry vector search and BM25 without new Signal types. Each answers only queries in its own language (can_serve), so they never answer each other's. The cache is a stock InMemoryEngram.
from cosmonapse import InMemoryEngram from engram.keyword_engram import KeywordEngram from engram.vector_engram import VectorEngram VECTORS = VectorEngram(engram_id="rag-vectors", engram_kind="semantic") KEYWORDS = KeywordEngram(engram_id="rag-keywords", engram_kind="lexical") CACHE = InMemoryEngram(engram_id="rag-cache", engram_kind="context")
class VectorEngram(Engram): """In-memory vector store behind the Engram protocol. Entry shape (imprint): {"text": str, "doc_id": str, "chunk_index": int, "embedding": list[float], ...any metadata} Query shape (recall): {"embedding": list[float], "top_k": int = 4} filters: {"doc_id": str} narrows to one document. """ def __init__( self, *, engram_id: str = "rag-vectors", engram_kind: str = "semantic", version: str | None = "0.1.0", ) -> None: self.engram_id = engram_id self.engram_kind = engram_kind self.capabilities = ["vector", "cosine", "merge_key"] self.version = version self._entries: dict[str, dict[str, Any]] = {} self._merge_keys: dict[str, str] = {} # merge_key -> entry id self._lock = asyncio.Lock() # -- lifecycle ----------------------------------------------------- async def connect(self) -> None: ... async def close(self) -> None: self._entries.clear() self._merge_keys.clear() # -- capability negotiation ----------------------------------------- async def can_serve(self, query: dict[str, Any]) -> bool: return "embedding" in query # -- read ------------------------------------------------------------ async def recall( self, query: dict[str, Any], *, filters: dict[str, Any] | None = None, context_ref: str | None = None, deadline_ms: int | None = None, min_confidence: float | None = None, ) -> list[Hit]: t0 = time.monotonic() qvec = query.get("embedding") if not qvec: return [] top_k = int(query.get("top_k", 4)) async with self._lock: items = list(self._entries.items()) scored: list[Hit] = [] for eid, entry in items: if filters and any(entry.get(k) != v for k, v in filters.items()): continue score = cosine(qvec, entry.get("embedding", [])) if min_confidence is not None and score < min_confidence: continue # Strip the embedding from the wire reply - it is bulky and # the caller only needs text + metadata + score. slim = {k: v for k, v in entry.items() if k != "embedding"} scored.append(Hit(id=eid, entry=slim, score=score)) if deadline_ms and (time.monotonic() - t0) * 1000 > deadline_ms: break scored.sort(key=lambda h: h.score, reverse=True) return scored[:top_k] # ... imprint(): add / merge / upsert / delete, with a per-trace saga journal
One chunk, two indexes.
The ingester chunks and embeds, then imprints every chunk into both indexes under the same merge_key, so re-ingesting a document upserts instead of duplicating.
from cosmonapse import Axon, EngramBinding from config import hf_token from embeddings import chunk_text, embed async def ingest_neuron(input, context, *, imprint): """{"doc_id", "text"} -> {"doc_id", "chunks"}. Dual-writes both indexes.""" doc_id = input["doc_id"] chunks = chunk_text(input["text"]) vectors = await embed(chunks, api_key=hf_token()) for i, (chunk, vec) in enumerate(zip(chunks, vectors)): last = i == len(chunks) - 1 entry = {"doc_id": doc_id, "chunk_index": i, "text": chunk} await imprint( "vectors", op="upsert", entry={**entry, "embedding": vec}, merge_key=f"{doc_id}:{i}", await_ack=last, deadline_ms=2000, ) await imprint( "keywords", op="upsert", entry=entry, merge_key=f"{doc_id}:{i}", await_ack=last, deadline_ms=2000, ) return {"doc_id": doc_id, "chunks": len(chunks)} AXON = Axon( neuron_id="ingester", neuron_fn=ingest_neuron, capabilities=["rag-ingest"], engrams=[EngramBinding(name="vectors", directed_id="rag-vectors"), EngramBinding(name="keywords", directed_id="rag-keywords")], )
Recall both, fuse the ranks.
The retriever checks the answer cache, then recalls from both indexes in their native query shapes and merges the ranked lists with reciprocal-rank fusion.
from cosmonapse import Axon, EngramBinding from config import FETCH_K, MIN_SCORE, RRF_K, TOP_K, cache_key, hf_token from embeddings import embed async def retrieve_neuron(input, context, *, recall): """{"question", "top_k"?} -> {"cache_hit"} | {"candidates": [...]}. Hybrid recall: semantic (vectors) + lexical (keywords), fused with reciprocal-rank fusion. Checks the answer cache first. """ question = input["question"] top_k = int(input.get("top_k", TOP_K)) # 0. Answer-cache short-circuit (exact normalised question). cached = await recall( "cache", query={"merge_key": cache_key(question)}, deadline_ms=500, ) if cached.hits: return {"cache_hit": True, "answer": cached.hits[0].entry["content"]} # 1. Both backends in their native query language. qvec = (await embed([question], api_key=hf_token()))[0] sem = await recall( "vectors", query={"embedding": qvec, "top_k": FETCH_K}, min_confidence=MIN_SCORE, deadline_ms=2000, ) lex = await recall( "keywords", query={"text": question, "top_k": FETCH_K}, deadline_ms=2000, ) # 2. Reciprocal-rank fusion across the two ranked lists. fused: dict[str, dict] = {} for hits in (sem.hits, lex.hits): for rank, h in enumerate(hits): key = f"{h.entry['doc_id']}#{h.entry['chunk_index']}" slot = fused.setdefault(key, { "id": key, "doc_id": h.entry["doc_id"], "chunk_index": h.entry["chunk_index"], "text": h.entry["text"], "rrf": 0.0, }) slot["rrf"] += 1.0 / (RRF_K + rank + 1) candidates = sorted(fused.values(), key=lambda c: c["rrf"], reverse=True) return {"cache_hit": False, "candidates": candidates[: top_k * 2]} AXON = Axon( neuron_id="retriever", neuron_fn=retrieve_neuron, capabilities=["rag-retrieve"], engrams=[EngramBinding(name="vectors", directed_id="rag-vectors"), EngramBinding(name="keywords", directed_id="rag-keywords"), EngramBinding(name="cache", directed_id="rag-cache")], )
A cheap rescore, a grounded answer, cached on the way out.
The reranker is lexical overlap; replace it with a cross-encoder Neuron and nothing else changes. The generator answers from the chunks only and upserts the answer into the same cache the retriever reads.
from cosmonapse import Axon from config import RRF_K, TOP_K from engram.keyword_engram import tokenize async def rerank_neuron(input, context): """{"question", "candidates", "top_k"?} -> {"chunks": [...]}.""" qterms = set(tokenize(input["question"])) top_k = int(input.get("top_k", TOP_K)) rescored = [] for c in input["candidates"]: terms = set(tokenize(c["text"])) overlap = len(qterms & terms) / (len(qterms) or 1) rescored.append({**c, "score": round(0.7 * c["rrf"] * RRF_K + 0.3 * overlap, 4)}) rescored.sort(key=lambda c: c["score"], reverse=True) return {"chunks": rescored[:top_k]} AXON = Axon( neuron_id="reranker", neuron_fn=rerank_neuron, capabilities=["rag-rerank"], )
from cosmonapse import Axon, EngramBinding, Neuron from config import GEN_MODEL, SYSTEM_PROMPT, cache_key, hf_token def make_generate_neuron(): """{"question", "chunks"} -> {"answer", "sources"}. Caches the answer.""" llm = Neuron( source="huggingface", endpoint="https://router.huggingface.co", model=GEN_MODEL, api_key=hf_token(), use_chat_api=True, max_new_tokens=512, temperature=0.2, ) async def generate_neuron(input, context, *, imprint): question = input["question"] chunks = input["chunks"] if not chunks: return {"answer": "No relevant context found in the index.", "sources": []} blocks = [f"[{c['id']}]\n{c['text']}" for c in chunks] messages = [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": ( "Context:\n\n" + "\n\n---\n\n".join(blocks) + f"\n\nQuestion: {question}" )}, ] out = await llm({"messages": messages}, []) answer = out["response"].strip() await imprint( "cache", op="upsert", entry={"content": answer, "tags": ["answer"]}, merge_key=cache_key(question), await_ack=False, ) sources = [{"id": c["id"], "doc_id": c["doc_id"], "chunk_index": c["chunk_index"], "score": c["score"]} for c in chunks] return {"answer": answer, "sources": sources} return generate_neuron def make_axon() -> Axon: return Axon( neuron_id="generator", neuron_fn=make_generate_neuron(), capabilities=["rag-generate"], engrams=[EngramBinding(name="cache", directed_id="rag-cache")], )
Three TASKs, one trace.
ask_pipeline chains the stages with an explicit trace_id and parent_id, so Prism shows one lineage per question. Both interfaces call it from the node they are mounted on.
async def ask_pipeline(orchestrator, question: str, *, top_k: int = TOP_K, timeout_s: float = 60.0) -> dict: """retrieve -> rerank -> generate, chained on ONE trace_id, so Prism shows the whole workflow as a single lineage.""" tid = new_trace_id() r = await orchestrator.dispatch_and_wait( neuron="retriever", input={"question": question, "top_k": top_k}, trace_id=tid, timeout_s=timeout_s, ) out = r.payload["output"] if out.get("cache_hit"): return {"answer": out["answer"], "sources": [], "cached": True} r = await orchestrator.dispatch_and_wait( neuron="reranker", input={"question": question, "candidates": out["candidates"], "top_k": top_k}, trace_id=tid, parent_id=r.id, timeout_s=timeout_s, ) chunks = r.payload["output"]["chunks"] r = await orchestrator.dispatch_and_wait( neuron="generator", input={"question": question, "chunks": chunks}, trace_id=tid, parent_id=r.id, timeout_s=timeout_s, ) return {**r.payload["output"], "cached": False}
A terminal and an API onto the same nodes.
POST /ingest is the ApiReceptor's own endpoint: one TASK, with send, wait and stream modes. /ask and /stats are extra routes, because the pipeline is three TASKs rather than one. See Receptors for the interface layer on its own.
from cosmonapse import CliReceptor from engram.indexes import VECTORS from helpers import ask_pipeline, ingest_samples, stats as index_stats RECEPTOR = CliReceptor( prog="rag", description="Hybrid-retrieval RAG over sample_docs/.", timeout_s=120.0, ) @RECEPTOR.command(local=True, default=True, help="ask a question") async def ask(question: str): if VECTORS.size == 0: for doc in await ingest_samples(RECEPTOR.dendrite): print(f" indexed {doc['doc_id']:<24} {doc['chunks']} chunks") out = await ask_pipeline(RECEPTOR.dendrite, question) lines = [("(cached) " if out["cached"] else "") + out["answer"]] if out["sources"]: lines.append("sources: " + ", ".join(f"{s['id']} ({s['score']})" for s in out["sources"])) return "\n".join(lines) @RECEPTOR.command(local=True, help="index sizes") def stats(): return index_stats()
from cosmonapse import ApiReceptor from engram.indexes import VECTORS from helpers import ask_pipeline, ingest_samples, stats as index_stats RECEPTOR = ApiReceptor(neuron="ingester", path="/ingest", timeout_s=120.0) @RECEPTOR.route("/ask", methods=["POST"]) async def ask(body: dict): if VECTORS.size == 0: await ingest_samples(RECEPTOR.dendrite) return await ask_pipeline(RECEPTOR.dendrite, body["question"], top_k=int(body.get("top_k", 4))) @RECEPTOR.route("/stats") async def stats(): return index_stats()
Nine nodes, three builders.
import asyncio import contextlib import signal from cosmonapse import Dendrite, MemorySynapse, connect_synapse, run_brain from config import NAMESPACE, SYNAPSE_URL from engram.indexes import CACHE, KEYWORDS, VECTORS from neurons import generator, ingester, reranker, retriever from receptors import api, terminal def build_engram(synapse, engram) -> Dendrite: node = Dendrite(synapse=synapse, namespace=NAMESPACE, dendrite_id=f"{engram.engram_id}-node", role="worker") node.attach_engram(engram) return node def build_neuron(synapse, axon) -> Dendrite: node = Dendrite(synapse=synapse, namespace=NAMESPACE, dendrite_id=f"{axon.neuron_id}-node", role="worker") node.attach_axon(axon) return node def build_interface(synapse, name: str, receptor) -> Dendrite: """Interfaces run the pipeline, so they sit on orchestrator nodes.""" node = Dendrite(synapse=synapse, namespace=NAMESPACE, dendrite_id=f"{name}-node", heartbeat_s=0) node.attach_receptor(receptor) return node
main() passes every Engram, every Neuron and both interfaces to run_brain. Set RAG_GEN_MODEL, RAG_TOP_K, RAG_FETCH_K or RAG_MIN_SCORE to tune.
Same code, different transport.
With SYNAPSE_URL unset the brain runs on an in-process bus. Point it at a synapse and nothing in the modules changes. Prism attaches to any shared synapse and animates every Signal as it crosses.
# terminal 1 - a dev synapse (TCP, single host) $ cosmo synapse start memory --namespace=rag # terminal 2 - the same brain, over that synapse $ SYNAPSE_URL=cosmo://127.0.0.1:7070 python brain.py "What is a Dendrite?" # terminal 3 - Prism, the live view (http://127.0.0.1:7071) $ cosmo prism --url=cosmo://127.0.0.1:7070 -n rag # production transports - only the URL changes $ SYNAPSE_URL=nats://127.0.0.1:4222 python brain.py "What is a Dendrite?" $ SYNAPSE_URL=kafka://127.0.0.1:9092 python brain.py "What is a Dendrite?"