# 🕸️ Entity Graph K* — Linking Cross-Collection
## 🤔 ¿Qué hago? ¿Cómo lo hago? ¿Y para qué lo hago?
**¿Qué hago?** Proporciono una capa de **entity linking** sobre las tres colecciones Qdrant del proxy K* (`klaude_knowledge`, `klaude_sources`, `klaude_cache`). Extraigo entidades nombradas de los chunks indexados y construyo un grafo en memoria que conecta chunks relacionados entre colecciones distintas.
**¿Cómo lo hago?** Uso un modelo Ollama local (por defecto `qwen2.5:3b`) para extraer entidades de cada chunk durante el rebuild. Las entidades se almacenan en el payload de Qdrant bajo el campo `graph_entities: [...]`. En tiempo de consulta, tras el vector search paralelo de KB y Sources, busco chunks relacionados por entidades compartidas y los añado al contexto RAG.
**¿Y para qué lo hago?** Para que una pregunta sobre "klaude-proxy" no solo encuentre chunks con alta similitud coseno, sino también chunks sobre "Qdrant", "cache semántico" o "RAG pipeline" que estén conectados conceptualmente aunque su similitud vectorial sea menor. El resultado es un contexto RAG más rico y cross-collection.
---
## 🏗️ Arquitectura
```mermaid
flowchart TD
A[/v1/messages — query] --> B[Embedding]
B --> C{Vector Search Paralelo}
C --> D[klaude_knowledge<br/>search_with_expansion]
C --> E[klaude_sources<br/>search]
D --> F[kb_chunks]
E --> G[src_chunks]
F & G --> H[graph.expand_chunks]
H --> I{graph built?}
I -- No --> J[retorna vacío]
I -- Sí --> K[Colecta graph_entities<br/>de los chunks]
K --> L[Busca point_ids<br/>relacionados en índice]
L --> M[Qdrant retrieve<br/>puntos adicionales]
M --> N[graph_chunks]
F --> O[_rag_blocks]
G --> O
N --> O
O --> P[System prompt enriquecido]
R[POST /graph/rebuild] --> S[EntityExtractor<br/>Ollama NER]
S --> T[Scroll klaude_knowledge<br/>+ klaude_sources]
T --> U[set_payload graph_entities]
U --> V[In-memory index<br/>entity → set of point_ids]
```
---
## 📁 Módulo: `proxy/graph.py`
### Clases principales
| Clase | Responsabilidad |
|---|---|
| `EntityExtractor` | Llama a Ollama `/api/generate` con prompt NER, parsea JSON array de entidades |
| `EntityGraph` | Índice in-memory `dict[str, set[tuple[str, str]]]` — entity → {(collection, point_id)} |
### Funciones de módulo
| Función | Descripción |
|---|---|
| `get_graph()` | Singleton `EntityGraph` |
| `get_extractor()` | Singleton `EntityExtractor` |
| `build_rag_context(chunks)` | Formatea chunks expandidos como bloque Markdown para system prompt |
### Métodos de `EntityGraph`
| Método | Descripción |
|---|---|
| `expand_chunks(initial_chunks, client)` | Devuelve SOLO los chunks adicionales encontrados vía entity graph |
| `rebuild(extractor)` | Escanea colecciones, extrae entidades, actualiza payloads, reconstruye índice |
| `stats()` | Devuelve estado del grafo (built, unique_entities, total_links) |
---
## ⚙️ Configuración
Variables en `.env` (o por defecto en `config.py`):
| Variable | Default | Descripción |
|---|---|---|
| `GRAPH_OLLAMA_MODEL` | `qwen2.5:3b` | Modelo Ollama para extracción NER |
| `GRAPH_MAX_EXPANSION` | `5` | Máximo de chunks adicionales por request |
| `OLLAMA_BASE_URL` | `http://localhost:11434` | URL base Ollama (compartida con el resto del proxy) |
> ⚠️ El modelo `qwen2.5:3b` debe estar disponible en Ollama: `ollama pull qwen2.5:3b`
---
## 🔌 Endpoints
### `GET /graph/stats`
Devuelve el estado actual del grafo en memoria.
```json
{
"built": true,
"unique_entities": 1247,
"total_links": 8932,
"collections_indexed": ["klaude_knowledge", "klaude_sources"]
}
```
### `POST /graph/rebuild`
Requiere header `x-api-key`. Dispara un rebuild completo (puede tardar varios minutos dependiendo del volumen de chunks).
```bash
curl -X POST http://localhost:8080/graph/rebuild -H "x-api-key: your-key"
```
Respuesta:
```json
{
"collections": {
"klaude_knowledge": {"points": 450, "updated": 120, "errors": 0},
"klaude_sources": {"points": 89, "updated": 34, "errors": 0}
},
"total_points": 539,
"updated_points": 154,
"errors": 0
}
```
---
## 🔄 Flujo de datos en `/v1/messages`
```
query_vector → asyncio.gather(kb.search_with_expansion, src.search)
→ kb_chunks + src_chunks
→ graph.expand_chunks(all_chunks) ← NUEVO
→ _rag_blocks = [CACHE?, KB?, SOURCES?, GRAPH?]
→ system prompt enriquecido
→ X-Cache: KNOWLEDGE+SOURCES+GRAPH-RAG
```
El header `X-Cache` reflejará el source `GRAPH` cuando el entity graph aporte chunks adicionales.
---
## 📦 Payload Qdrant — campo `graph_entities`
Tras el rebuild, cada punto en `klaude_knowledge` y `klaude_sources` tendrá:
```json
{
"content": "...",
"file_path": "proxy/cache.py",
"graph_entities": ["qdrant", "asyncqdrantclient", "get_client", "cache", "similarity_threshold"]
}
```
Este campo es idempotente — rebuild nunca sobreescribe entidades ya existentes, solo añade las que faltan.
---
## 🚀 Comportamiento en startup
Al arrancar el proxy, se lanza un background task no bloqueante que ejecuta `graph.rebuild()`. Si Ollama no está disponible en ese momento, el rebuild falla silenciosamente (log `WARNING`) y el grafo queda vacío. En ese caso, `expand_chunks()` devuelve lista vacía sin afectar el pipeline RAG.
---
## 🔗 Documentos relacionados
- [proxy.md](proxy.md) — arquitectura general del proxy y pipeline RAG
- [knowledge_base.md](knowledge_base.md) — colección `klaude_knowledge`, hybrid search, indexación
- [sources.md](sources.md) — colección `klaude_sources`, ingesta web/markdown/PDF
- [cache.md](cache.md) — colección `klaude_cache`, semantic cache Q&A