Arquitectura¶
Documento técnico de referencia principal. Claude Code (y cualquier otro agente de código) debe leer esto antes de escribir una sola línea.
Stack¶
| Componente | Tecnología | Versión | Razón |
|---|---|---|---|
| Backend por defecto | SQLite + sqlite-vec |
3.40+ / 0.1+ | Almacenamiento portátil sin dependencias (DDR-004) |
| Backend opcional | Neo4j Community | 5.26.24 LTS | Producción multiproceso, índices vectoriales grandes |
| Lenguaje | Python | ≥ 3.11 | Ecosistema de agentes, compatibilidad con FastMCP |
| Gestión de deps. | uv | latest | Estándar moderno, rápido |
| Adaptador MCP | FastMCP + async stores | nativo | Stores basados en protocolos, cero Cypher en herramientas |
| Adaptador Obsidian | stdio local | — | Sincronización documento ↔ grafo |
| Embeddings | HTTP OpenAI-compat | — | Ollama, OpenAI, LM Studio, vLLM, llama.cpp, Jina (DDR-004) |
| HTTP asíncrono | httpx | ≥ 0.27 | Llamadas de embedding no bloqueantes en el servidor MCP |
| Contenedor (solo Neo4j) | Docker Desktop | latest | Infraestructura Neo4j reproducible |
| CI/CD | GitHub Actions | — | Tests y publicación en PyPI |
| Empaquetado | pyproject.toml | — | uv sync (base) / uv sync --extra neo4j (opt-in); publicación en PyPI prevista |
Qué hace diferente a Engrama¶
Engrama no es otro wrapper MCP para una sola base de datos. Es un framework cognitivo que combina dos capas de memoria complementarias:
- Vault de Obsidian — memoria narrativa (documentos, razonamiento, contexto completo).
- Grafo de conocimiento — memoria relacional (entidades, relaciones, patrones). Respaldado por SQLite por defecto, o Neo4j cuando la escala lo requiere. Modelo de datos idéntico en ambos.
Los skills reflect y proactive recorren el grafo para descubrir
conexiones que ninguna de las dos capas podría encontrar por separado.
Ejemplo: un Problema en el Proyecto B comparte un Concepto con un
Problema resuelto en el Proyecto A — Engrama lo detecta y propone la
Decisión existente como candidata a solución, sin que nadie se lo pida.
Diagrama de capas¶
block-beta
columns 1
block:adapters["Layer 1 · Adapters"]
columns 5
MCP["🔌 MCP Server\n(FastMCP)"]
Obsidian["📓 Obsidian\nSync"]
LangChain["🦜 LangChain"]
REST["🌐 REST API"]
SDK["📦 SDK"]
end
block:skills["Layer 2 · Skills"]
columns 6
remember["remember"]
recall["recall"]
associate["associate"]
forget["forget"]
reflect["⭐ reflect"]
proactive["⭐ proactive"]
end
block:engine["Layer 3 · Engine"]
columns 5
hybrid["HybridSearch\nEngine"]
temporal["Temporal\n(decay, valid_to)"]
security["Security\n(planned)"]
write["Write Pipeline\n(MERGE)"]
query["Query"]
end
block:protocols["Layer 4 · Protocols"]
columns 3
gs["GraphStore"]
vs["VectorStore"]
ep["EmbeddingProvider"]
end
block:backends["Layer 5 · Backends + Embeddings"]
columns 5
sqlite_be["sqlite/\n(default)"]
neo4j_be["neo4j/\n(opt-in)"]
null_be["null/\n(testing)"]
openai_compat["OpenAI-compat\n(Ollama, OpenAI, ...)"]
null_embed["NullProvider"]
end
block:storage["Storage"]
columns 3
sqlite[("SQLite + sqlite-vec\n~/.engrama/engrama.db")]
neo4j[("Neo4j 5.26 LTS\nbolt://7687")]
vault[("Obsidian Vault\nVAULT_PATH")]
end
adapters --> skills
skills --> engine
engine --> protocols
protocols --> backends
backends --> storage
La factoría en engrama/backends/__init__.py lee GRAPH_BACKEND y
devuelve la implementación correspondiente. Los skills, adaptadores y el
motor solo hablan con los protocolos — no saben qué backend hay debajo.
Consulta DDR-004 para la justificación y backends.md
para la guía de elección orientada al usuario.
Flujo de datos: reflect → Insight¶
flowchart LR
subgraph Graph["Knowledge graph (SQLite or Neo4j)"]
P1[Project A] -->|HAS| RP[Problem\nresolved]
P2[Project B] -->|HAS| OP[Problem\nopen]
RP -->|APPLIES| C((Concept))
OP -->|APPLIES| C
RP -->|SOLVED_BY| D[Decision]
P1 -->|INFORMED_BY| D
end
subgraph Reflect["⭐ reflect skill"]
Q1["Query 1\nCross-project\nsolution"]
Q2["Query 2\nShared\ntechnology"]
Q3["Query 3\nTraining\nopportunity"]
end
subgraph Output["Output"]
I[/"💡 Insight node\nstatus: pending\nconfidence: 0.8"/]
end
Graph -.->|pattern\ndetected| Reflect
Reflect -->|MERGE| Output
style C fill:#f9d71c,stroke:#333,color:#333
style I fill:#a8e6cf,stroke:#333,color:#333
style D fill:#ffd3b6,stroke:#333,color:#333
El skill reflect emite nodos Insight idénticos independientemente del
backend. La detección de patrones en Neo4j usa Cypher; en SQLite cada
patrón es una consulta SQL traducida a mano que devuelve las mismas
filas. La suite de contratos en tests/contracts/ garantiza la
equivalencia.
Esquema del grafo¶
erDiagram
Project ||--o{ Technology : USES
Project ||--o{ Decision : INFORMED_BY
Project ||--o{ Problem : HAS
Project ||--o{ Client : FOR
Problem ||--o{ Concept : APPLIES
Problem ||--o{ Decision : SOLVED_BY
Course ||--o{ Concept : COVERS
Course ||--o{ Technology : TEACHES
Course ||--o{ Client : FOR
Project {
string name PK
string status
string repo
string stack
string description
}
Decision {
string title PK
string rationale
string alternatives
}
Problem {
string title PK
string status
string solution
string context
}
Technology {
string name PK
string version
string type
}
Concept {
string name PK
string domain
}
Course {
string name PK
string cohort
string level
}
Client {
string name PK
string sector
}
Insight {
string title PK
string body
float confidence
string status
string source_query
}
El esquema se define en profiles/*.yaml y se aplica al backend que
esté activo. SQLite codifica las etiquetas en una columna label de la
tabla nodes; Neo4j usa etiquetas de nodo nativas. Desde el punto de
vista de la aplicación, esto es transparente.
Estructura de directorios¶
engrama/
├── README.md
├── README_ES.md
├── vision.md
├── architecture.md
├── backends.md # ★ NUEVO (DDR-004) — guía de elección de backend
├── graph-schema.md
├── roadmap.md
├── contributing.md
├── changelog.md
├── ddr-001.md … ddr-004.md
├── pyproject.toml
├── docker-compose.yml # Solo Neo4j — no necesario para SQLite por defecto
├── .env.example
│
├── engrama/
│ ├── __init__.py
│ │
│ ├── core/
│ │ ├── client.py # Wrapper del driver Neo4j (síncrono)
│ │ ├── engine.py # Pipeline de escritura síncrono (MERGE+timestamps)
│ │ ├── protocols.py # GraphStore / VectorStore / EmbeddingProvider
│ │ ├── schema.py # Dataclasses Python para nodos y relaciones
│ │ ├── search.py # HybridSearchEngine — scoring multi-señal
│ │ ├── temporal.py # Confidence decay, days_since, temporal_score
│ │ └── text.py # Re-export de node_to_text
│ │
│ ├── backends/ # ★ DDR-004: backends enchufables
│ │ ├── __init__.py # Factoría create_stores() / create_async_stores()
│ │ ├── null.py # NullGraphStore / NullVectorStore (testing)
│ │ ├── sqlite/ # ★ NUEVO — backend por defecto
│ │ │ ├── store.py # SqliteGraphStore (síncrono)
│ │ │ ├── async_store.py # SqliteAsyncStore — mismo contrato que Neo4jAsyncStore
│ │ │ ├── vector.py # SqliteVecStore — virtual table de sqlite-vec
│ │ │ └── schema.sql # Se aplica automáticamente en la primera conexión
│ │ └── neo4j/ # Opt-in vía `uv sync --extra neo4j`
│ │ ├── backend.py # Neo4jGraphStore (síncrono) — SDK / CLI
│ │ ├── async_store.py # Neo4jAsyncStore (asíncrono) — servidor MCP
│ │ └── vector.py # Neo4jVectorStore — operaciones de índice vectorial
│ │
│ ├── embeddings/
│ │ ├── __init__.py # Factoría create_provider()
│ │ ├── null.py # NullProvider (sin embeddings)
│ │ ├── ollama.py # Wrapper legacy de conveniencia
│ │ ├── openai_compat.py # ★ NUEVO — OpenAI / Ollama / LM Studio / vLLM / Jina (DDR-004)
│ │ └── text.py # node_to_text() — texto canónico para embedding
│ │
│ ├── skills/
│ │ ├── remember.py # MERGE entidad + observación
│ │ ├── recall.py # Búsqueda fulltext + recorrido del grafo
│ │ ├── associate.py # Crear relaciones entre entidades
│ │ ├── reflect.py # ★ Detección de patrones cross-entidad
│ │ ├── proactive.py # ★ Presenta Insights sin que se lo pidan
│ │ └── forget.py # Decaimiento, archivado, TTL
│ │
│ ├── adapters/
│ │ ├── mcp/ # Servidor FastMCP (cero Cypher en los handlers)
│ │ ├── obsidian/ # ★ Sincronización bidireccional vault ↔ grafo (DDR-002)
│ │ └── sdk/ # SDK Python de Engrama (context manager)
│ │
│ └── ingest/
│ ├── conversation.py
│ └── web.py
│
├── profiles/
│ ├── base.yaml # Base universal
│ ├── developer.yaml # Ejemplo standalone
│ └── modules/ # Módulos de dominio componibles
│
├── scripts/
│ └── init-schema.cypher # Solo Neo4j; SQLite usa backends/sqlite/schema.sql
│
├── examples/
│ ├── claude_desktop/
│ └── langchain_agent/
│
└── tests/
├── conftest.py
├── contracts/ # ★ Parametrizados sobre ambos backends
│ ├── test_graphstore_contract.py # Stores síncronos
│ └── test_async_graphstore_contract.py # Stores asíncronos (DDR-004)
├── backends/
│ ├── test_sqlite.py
│ ├── test_sqlite_async.py
│ └── test_sqlite_vector.py
├── test_core.py
├── test_skills.py
├── test_adapters.py
├── test_obsidian_sync.py
├── test_phase4_skills.py
├── test_proactive.py
├── test_protocols.py
├── test_sdk.py
├── test_cli.py
├── test_composable.py
├── test_embeddings.py
├── test_openai_compat_embedder.py # ★ NUEVO
├── test_hybrid_search.py
├── test_neo4j_store.py # Integración async con Neo4j
├── test_temporal.py
└── test_vector_store.py
Capa de protocolos y backends¶
Todas las operaciones de almacenamiento pasan por protocolos abstractos
definidos en core/protocols.py: GraphStore, VectorStore y
EmbeddingProvider. Ningún adaptador, skill ni herramienta escribe
Cypher o SQL directamente — todo pasa por una implementación del
backend.
Hay dos pares de implementaciones de backend:
Stores síncronos (usados por el SDK y la CLI a través de EngramaEngine)¶
SqliteGraphStore(backends/sqlite/store.py) — por defecto. Python puro sobresqlite3. ElSqliteVecStorecomparte la misma conexión, así que los vectores viven en el mismo archivo.db.Neo4jGraphStore(backends/neo4j/backend.py) — envuelveEngramaClient(driverneo4jsíncrono).
Stores asíncronos (usados por el servidor MCP)¶
SqliteAsyncStore(backends/sqlite/async_store.py) — envuelve el store SQLite síncrono y traduce la forma de retorno de cada método para que los handlers MCP reciban la misma estructura de diccionario independientemente del backend.Neo4jAsyncStore(backends/neo4j/async_store.py) — envuelveneo4j.AsyncDriver. Contiene todo el Cypher de las herramientas MCP. El propioserver.pyno contiene ni una cadena Cypher.
NullGraphStore y NullVectorStore existen para testing y modo
dry-run. Se pueden añadir nuevos backends (NebulaGraph, ArcadeDB,
pgvector, Chroma, LEANN, ...) implementando los mismos protocolos.
Las factorías create_stores() y create_async_stores() en
backends/__init__.py leen GRAPH_BACKEND / VECTOR_BACKEND del
entorno (o de un dict de configuración explícito) y devuelven las
implementaciones adecuadas.
Las suites de contratos¶
Dos suites parametrizadas de pytest residen en tests/contracts/:
test_graphstore_contract.py— ejecuta todos los tests de comportamiento contra ambos stores síncronos.test_async_graphstore_contract.py— ejecuta todos los tests de comportamiento contra ambos stores asíncronos.
Los tests de Neo4j se saltan si NEO4J_PASSWORD no está configurado,
así que la suite solo-SQLite (76 tests) pasa en un checkout limpio sin
.env. Juntas detectaron tres bugs de deriva que se descubrieron y
corrigieron durante la implementación de DDR-004; las suites existen
para asegurar que no reaparezcan.
Embeddings (DDR-003 Fase B + DDR-004)¶
EmbeddingProvider está implementado por:
OpenAICompatibleProvider(embeddings/openai_compat.py) — habla el formato HTTP/v1/embeddingsde OpenAI. Funciona con OpenAI directamente, Ollama (OPENAI_BASE_URL=http://localhost:11434/v1), LM Studio, vLLM, llama.cpp, Jina, o cualquier otro servicio compatible. Métodos síncronos (embed,embed_batch) y asíncronos (aembed,aembed_batch), ambos usanhttpx.OllamaProvider(embeddings/ollama.py) — wrapper legacy de conveniencia. Usa el endpoint nativo/api/embeddingsde Ollama. Se mantiene por compatibilidad con archivos.envexistentes.NullProvider(embeddings/null.py) — no-op,dimensions=0. Se usa cuandoEMBEDDING_PROVIDER=none(por defecto). Tiene métodos síncronos y asíncronos.
node_to_text() en embeddings/text.py construye la cadena de texto
que se embebe.
Embed-on-write: cuando hay un proveedor de embeddings activo,
engrama_remember y engrama_sync_note embeben automáticamente cada
nodo tras el merge. El vector se almacena:
- SQLite: en la virtual table
vec0node_embeddings(mismo archivo.db). - Neo4j: como propiedad
n.embedding; los nodos reciben una etiqueta secundaria:Embeddedpara que el índice vectorial cubra todos los tipos de nodo.
Búsqueda híbrida (DDR-003 Fase C)¶
HybridSearchEngine (core/search.py) fusiona señales de fulltext +
vectorial + boost por grafo + temporalidad. Dispone de métodos síncronos
(search()) y asíncronos (asearch()). Fórmula de puntuación:
final = α × vector + (1-α) × fulltext + β × graph_boost + γ × temporal
Cuando EMBEDDING_PROVIDER=none, α se fuerza a 0 — fulltext puro con
boost por grafo opcional. Degradación elegante: si el servicio de
embeddings no responde, la rama vectorial se omite silenciosamente.
Tanto los stores síncronos como los asíncronos exponen search_similar
devolviendo una forma uniforme {node_id, label, name, score, summary,
tags, confidence, updated_at} para que el scorer pueda rellenar campos
de enriquecimiento sin una segunda ida al backend — una regresión
descubierta durante las pruebas de DDR-004 (ver DDR-004
"Riesgos").
Razonamiento temporal (DDR-003 Fase D)¶
Cada nodo lleva metadatos temporales que habilitan el decaimiento de confianza, la supersesión de hechos y las consultas de viaje en el tiempo:
valid_from(datetime) — cuándo el hecho pasó a ser verdadero. Se establece automáticamente en la creación.valid_to(datetime) — cuándo el hecho fue supersedido.null= sigue siendo verdadero.confidence(float, 0.0–1.0) — decae con el tiempo. Por defecto 1.0.decayed_at(datetime) — último pase de decaimiento.created_at,updated_at— marcas temporales del sistema (gestionadas automáticamente).
Decaimiento de confianza (engrama decay): decaimiento exponencial
new_conf = conf × exp(-rate × days_since_update).
Supersesión (valid_to): establecerlo reduce automáticamente la
confianza a la mitad. Actualizar un nodo supersedido limpia valid_to
(revivir) y registra una advertencia de conflicto.
Consultas temporales (query_at_date): devuelve nodos donde
valid_from <= date AND (valid_to IS NULL OR valid_to >= date).
Puntuación temporal en la búsqueda híbrida: el término γ × temporal
combina confianza con recencia.
temporal_score = confidence × 2^(-days / half_life).
Por defecto γ=0.1 y half_life=30 días.
Integración con Obsidian (DDR-002)¶
El vault es la capa narrativa. El grafo es la capa relacional. Ninguno sustituye al otro.
Integridad referencial vía engrama_id¶
Cada nodo documentado lleva engrama_id en el frontmatter YAML de su
nota. adapters/obsidian/sync.py mantiene el contrato:
sequenceDiagram
participant V as Obsidian Vault
participant A as ObsidianAdapter
participant P as NoteParser
participant E as Engine (MERGE)
participant N as Backend (SQLite or Neo4j)
rect rgb(230, 245, 255)
Note over V,N: Nota creada o modificada
V->>A: read_note(path)
A->>P: parse(content, frontmatter)
P-->>E: ParsedNote (label, name, props)
E->>N: MERGE node
N-->>E: engrama_id
E->>A: inject_engrama_id(path, id)
A->>V: write frontmatter
end
rect rgb(255, 235, 235)
Note over V,N: Nota eliminada
V--xA: note missing
A->>E: archive_missing()
E->>N: SET status = "archived"
Note right of N: Nunca se borra físicamente
end
Sincronización bidireccional¶
DDR-002 establece que cada relación se replica en el mapa relations
del frontmatter de la nota origen. Combinado con DDR-004 (almacenamiento
portátil), esto significa que un vault de Obsidian es una copia de
seguridad portátil de todo el grafo: una instalación SQLite nueva
apuntando al mismo vault reconstruye el grafo completo ejecutando
engrama_sync_vault.
| Operación | Módulo | Propósito |
|---|---|---|
| Leer nota | adapter.py |
Extraer contenido + frontmatter |
| Buscar notas | adapter.py |
Encontrar notas relacionadas por texto |
| Listar notas | adapter.py |
Escaneo completo del vault |
| Inyectar engrama_id | adapter.py |
Identidad de sincronización bidireccional |
vault_create_note |
proactive.py |
Escribir notas de Insight de vuelta al vault |
vault_append_note |
proactive.py |
Añadir sección de insight a notas existentes |
Los skills distintivos: reflect + proactive + ingest¶
skills/reflect.py ejecuta detección de patrones cross-entidad
adaptativa. Antes de ejecutar cualquier patrón, perfila el grafo
(cuenta etiquetas con datos) y solo ejecuta patrones cuyas
precondiciones se cumplen. Siete patrones de detección:
- Solución cross-proyecto — Problemas que comparten Conceptos con Problemas resueltos en otros Proyectos.
- Tecnología compartida — cualesquiera dos entidades conectadas a la misma Tecnología vía USES/TEACHES/COMPOSED_OF.
- Oportunidad de formación — Vulnerabilidades o Problemas abiertos vinculados a Conceptos que un Curso cubre.
- Transferencia de técnica — Técnicas usadas en 2+ Dominios.
- Agrupación de conceptos — 3+ entidades que comparten un Concepto.
- Conocimiento obsoleto — nodos con >90 días de antigüedad O con confianza <0.3, aún vinculados a Proyectos o Cursos activos.
- Infraconectados — nodos con <2 relaciones.
Los resultados se escriben como nodos Insight con confianza escalada
por la fuerza de conexión y el recuento de entidades. Los Insights
previamente descartados Y aprobados nunca se resurgen — la ejecución
de reflect filtra contra dismissed | approved para que volver a
ejecutar reflect no deshaga la revisión humana (regresión detectada y
corregida durante las pruebas de DDR-004).
skills/proactive.py presenta los Insights pendientes al agente y los
escribe de vuelta en Obsidian vía vault_append_note. El agente propone
— el humano aprueba. Los Insights nunca se ejecutan automáticamente.
Disparadores de proactividad (estado a nivel de módulo en el servidor
MCP):
- Tras 10+ llamadas a engrama_remember desde el último reflect →
se devuelve proactive_hint.
- engrama_search comprueba si hay Insights pendientes relacionados
con la consulta.
- engrama_reflect reinicia el contador.
Ingesta (engrama_ingest): lee una nota del vault, texto en crudo o
transcripción de conversación y devuelve el contenido con guía de
extracción de entidades más pistas de deduplicación (nodos existentes en
el grafo). El agente entonces llama a engrama_remember por cada
entidad extraída — dirigido por el agente, no opaco.
Adaptador MCP¶
Servidor MCP nativo construido con FastMCP y el async store
correspondiente. Toda la lógica de almacenamiento reside en
*AsyncStore; los handlers de herramientas MCP se encargan solo de
orquestación, validación, E/S del vault y formateo de respuestas.
Doce herramientas:
engrama_status— introspección de solo lectura: ruta del vault, backend, embedder, modo de búsqueda, versión. Los agentes deben llamar a esto al inicio de sesión cuando Engrama coexiste con otros MCP capaces de acceder a Obsidian, para poder desambiguar a qué servidor se refiere "el vault" antes de cualquier sincronización.engrama_search— búsqueda híbrida en el grafo de memoriaengrama_remember— crear o actualizar un nodo (siempre MERGE)engrama_relate— crear una relación (gestiona nodos con clave title)engrama_context— recuperar el vecindario de un nodo hasta N saltosengrama_sync_note— sincronizar una nota de Obsidian con el grafo; aceptadry_run=truepara previsualizar el impacto sin escribirengrama_sync_vault— escaneo completo del vault, reconciliar todas las notas; aceptadry_run=truepara proyectar recuentos de creación/actualización y listar los archivos que recibirían una inyección deengrama_idengrama_ingest— leer contenido y devolver guía de extracciónengrama_reflect— detección adaptativa de patrones cross-entidad → nodos Insightengrama_surface_insights— leer Insights pendientes para presentación al agenteengrama_approve_insight— el humano aprueba o descarta un Insightengrama_write_insight_to_vault— añadir Insight aprobado a una nota de Obsidian
Forma de respuesta de engrama_status¶
Contrato JSON estable. Los campos están ausentes (en lugar de null)
cuando el subsistema correspondiente está desactivado, de modo que un
agente puede hacer if "path" in payload["vault"]: de forma fiable.
{
"version": "0.10.0",
"backend": {
"name": "sqlite",
"ok": true,
"node_count": 1234
},
"vault": {
"configured": true,
"path": "/abs/path/to/engrama/vault",
"note_count": 87
},
"embedder": {
"configured": true,
"provider": "ollama",
"model": "nomic-embed-text",
"dimensions": 768
},
"search": {
"mode": "hybrid",
"degraded": false,
"reason": ""
}
}
backend.name está normalizado — los async stores subyacentes reportan
sqlite-async / neo4j-async, pero la herramienta elimina el sufijo
-async ya que los agentes razonan sobre qué base de datos está
ejecutándose, no sobre la forma del SDK. search.degraded siempre es
false en las llamadas de status (la degradación se detecta durante
engrama_search); usa este campo para predecir qué intentaría la
próxima búsqueda.
El CLI del servidor MCP acepta un flag --backend (sqlite o neo4j)
más overrides por backend (--db-path, --neo4j-uri,
--neo4j-password, --vault-path). Los valores por defecto se toman
del entorno.
Sistema de perfiles¶
Los perfiles son la fuente única de verdad para el esquema del grafo. Hay dos modos: perfiles standalone y módulos componibles.
Standalone (un YAML, esquema completo):
uv run engrama init --profile developer
Componible (base + módulos de dominio, recomendado para usuarios multi-rol):
uv run engrama init --profile base --modules hacking teaching photography
El perfil base (profiles/base.yaml) define nodos universales:
Project, Concept, Decision, Problem, Technology, Person. Los módulos
de dominio en profiles/modules/ añaden nodos específicos del dominio
y pueden referenciar etiquetas base en sus relaciones. El motor de merge
une propiedades, deduplica relaciones y valida todos los endpoints.
Los usuarios pueden crear módulos para cualquier dominio — los módulos incluidos son ejemplos, no un conjunto fijo. El skill onboard genera módulos personalizados mediante una entrevista conversacional.
Referencia de configuración (.env)¶
| Variable | Por defecto | Descripción |
|---|---|---|
GRAPH_BACKEND |
sqlite |
sqlite, neo4j o null |
VECTOR_BACKEND |
coincide con graph | sqlite-vec, neo4j o none (automático si ausente) |
ENGRAMA_DB_PATH |
~/.engrama/engrama.db |
Archivo de base de datos SQLite |
NEO4J_URI |
bolt://localhost:7687 |
URI de conexión a Neo4j |
NEO4J_USERNAME |
neo4j |
Nombre de usuario de Neo4j |
NEO4J_PASSWORD |
— | Contraseña de Neo4j (requerida cuando GRAPH_BACKEND=neo4j) |
NEO4J_DATABASE |
neo4j |
Nombre de la base de datos Neo4j |
ENGRAMA_PROFILE |
developer |
Nombre de perfil para la generación del esquema |
VAULT_PATH |
~/Documents/vault |
Ruta raíz del vault de Obsidian |
EMBEDDING_PROVIDER |
none |
none, ollama u openai |
EMBEDDING_MODEL |
nomic-embed-text |
Nombre del modelo de embedding |
EMBEDDING_DIMENSIONS |
768 |
Tamaño del vector de embedding |
OPENAI_BASE_URL |
https://api.openai.com/v1 |
Endpoint OpenAI-compatible |
OPENAI_API_KEY |
— | Clave API (cuando sea necesaria) |
OLLAMA_URL |
http://localhost:11434 |
Endpoint de la API de Ollama (proveedor legacy) |
HYBRID_ALPHA |
0.6 |
Peso vectorial vs fulltext |
HYBRID_GRAPH_BETA |
0.15 |
Peso del boost por topología del grafo |
Reglas de implementación¶
- Siempre
MERGE, nuncaCREATEa secas — previene duplicados en ambos backends. - El índice fulltext es obligatorio —
memory_search(Neo4j) /nodes_fts(SQLite) sobre todas las propiedades de texto. - Marcas temporales en todas partes —
created_atyupdated_aten cada nodo. - Los embeddings son opcionales — la estructura del grafo es lo principal; la búsqueda semántica vía proveedores OpenAI-compatible mejora la búsqueda cuando está habilitada.
- Tests de integración contra ambos backends — sin mocks para la capa de datos; la suite de contratos se parametriza sobre SQLite y Neo4j.
- Siempre parámetros en Cypher y SQL — nunca formatear consultas con cadenas.
server.pycontiene cero cadenas de consulta — todas las consultas viven en el*AsyncStorecorrespondiente.- Los async stores traducen formas — delegación explícita
método a método, nunca un
__getattr__opaco (así es como se introdujo originalmente el bug de deriva del contrato; DDR-004 lo sustituyó).
Repositorios relacionados¶
scops/engrama— este framework.
Nota histórica: inicialmente se planificó una capa intermedia
mcp-neo4j, pero se descartó en favor de un servidor MCP nativo. Los drivers asíncronos dan control total sobre la lógica de MERGE, el manejo de parámetros y la selección de claves (name vs title) sin una dependencia adicional. DDR-004 generalizó el mismo enfoque para SQLite.