el-primor 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.opencode/skills/comprension-y-plan/SKILL.md +43 -0
- package/.opencode/skills/debug-forense/SKILL.md +43 -0
- package/.opencode/skills/diseno-excelente/SKILL.md +43 -0
- package/.opencode/skills/investigacion-actualizada/SKILL.md +43 -0
- package/.opencode/skills/reporting-profesional/SKILL.md +43 -0
- package/.opencode/skills/seguridad-primor/SKILL.md +43 -0
- package/.opencode/skills/verificacion-rigurosa/SKILL.md +43 -0
- package/AGENTS.md +244 -0
- package/INSTALL.md +70 -0
- package/LICENSE +21 -0
- package/README.md +148 -0
- package/bin/cli.js +176 -0
- package/install.sh +69 -0
- package/opencode.json +43 -0
- package/package.json +35 -0
- package/setup.sh +91 -0
- package/turbo-vec-mcp.py +267 -0
package/turbo-vec-mcp.py
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
turbo-vec-mcp.py — MCP Server para TurboVec (El Primor v3.0.0)
|
|
4
|
+
|
|
5
|
+
Servidor MCP (Model Context Protocol) que expone búsqueda semántica,
|
|
6
|
+
almacenamiento de documentos y estadísticas usando TurboVec + SQLite.
|
|
7
|
+
|
|
8
|
+
Tools:
|
|
9
|
+
- turbo_vec_search(query, k=5, collection=null)
|
|
10
|
+
- turbo_vec_store(title, content, collection="default", metadata_json="{}")
|
|
11
|
+
- turbo_vec_stats()
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import sys
|
|
16
|
+
import sqlite3
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
# ── Configuración ───────────────────────────────────────────────────────────
|
|
20
|
+
PRIMOR_DIR = Path.home() / ".primor"
|
|
21
|
+
DB_PATH = PRIMOR_DIR / "primor.db"
|
|
22
|
+
MODEL_NAME = "sentence-transformers/multilingual-e5-small"
|
|
23
|
+
MODEL_DIR = PRIMOR_DIR / "models" / "multilingual-e5-small"
|
|
24
|
+
|
|
25
|
+
# Estado global lazy
|
|
26
|
+
_model = None
|
|
27
|
+
_index = None
|
|
28
|
+
_db: sqlite3.Connection | None = None
|
|
29
|
+
|
|
30
|
+
# ── Inicialización lazy ────────────────────────────────────────────────────
|
|
31
|
+
def _ensure_model():
|
|
32
|
+
global _model, _index
|
|
33
|
+
if _model is None:
|
|
34
|
+
from sentence_transformers import SentenceTransformer
|
|
35
|
+
_model = SentenceTransformer(
|
|
36
|
+
str(MODEL_DIR) if MODEL_DIR.exists() else MODEL_NAME,
|
|
37
|
+
cache_folder=str(PRIMOR_DIR / "models"),
|
|
38
|
+
device="cpu",
|
|
39
|
+
)
|
|
40
|
+
if _index is None:
|
|
41
|
+
from turbo_vec import IdMapIndex
|
|
42
|
+
_index = IdMapIndex(dim=_model.get_sentence_embedding_dimension())
|
|
43
|
+
return _model, _index
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _ensure_db():
|
|
47
|
+
global _db
|
|
48
|
+
if _db is None:
|
|
49
|
+
_db = sqlite3.connect(str(DB_PATH))
|
|
50
|
+
_db.row_factory = sqlite3.Row
|
|
51
|
+
_db.execute("PRAGMA journal_mode=WAL")
|
|
52
|
+
return _db
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _embed(text: str) -> list[float]:
|
|
56
|
+
model, _ = _ensure_model()
|
|
57
|
+
return model.encode(text, normalize_embeddings=True).tolist()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ── Tools ───────────────────────────────────────────────────────────────────
|
|
61
|
+
def turbo_vec_search(query: str, k: int = 5, collection: str | None = None):
|
|
62
|
+
"""Búsqueda semántica en la base de conocimiento."""
|
|
63
|
+
_, index = _ensure_model()
|
|
64
|
+
db = _ensure_db()
|
|
65
|
+
|
|
66
|
+
query_vec = _embed(query)
|
|
67
|
+
results = index.search(query_vec, top_k=k)
|
|
68
|
+
|
|
69
|
+
output = []
|
|
70
|
+
for vec_id, score in results:
|
|
71
|
+
if score < 0.3:
|
|
72
|
+
continue
|
|
73
|
+
if collection:
|
|
74
|
+
row = db.execute(
|
|
75
|
+
"SELECT * FROM docs WHERE vec_id = ? AND collection = ?",
|
|
76
|
+
(vec_id, collection),
|
|
77
|
+
).fetchone()
|
|
78
|
+
else:
|
|
79
|
+
row = db.execute(
|
|
80
|
+
"SELECT * FROM docs WHERE vec_id = ?", (vec_id,)
|
|
81
|
+
).fetchone()
|
|
82
|
+
if row:
|
|
83
|
+
output.append({
|
|
84
|
+
"id": row["id"],
|
|
85
|
+
"vec_id": row["vec_id"],
|
|
86
|
+
"title": row["title"],
|
|
87
|
+
"content": row["content"][:500],
|
|
88
|
+
"collection": row["collection"],
|
|
89
|
+
"score": round(float(score), 4),
|
|
90
|
+
"created_at": row["created_at"],
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
return {"query": query, "k": k, "results": output, "total": len(output)}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def turbo_vec_store(
|
|
97
|
+
title: str,
|
|
98
|
+
content: str,
|
|
99
|
+
collection: str = "default",
|
|
100
|
+
metadata_json: str = "{}",
|
|
101
|
+
):
|
|
102
|
+
"""Almacena un documento con embedding semántico."""
|
|
103
|
+
model, index = _ensure_model()
|
|
104
|
+
db = _ensure_db()
|
|
105
|
+
|
|
106
|
+
vec = _embed(f"{title}\n{content}")
|
|
107
|
+
vec_id = index.add(vec)
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
metadata = json.loads(metadata_json)
|
|
111
|
+
except json.JSONDecodeError:
|
|
112
|
+
metadata = {}
|
|
113
|
+
|
|
114
|
+
cursor = db.execute(
|
|
115
|
+
"""INSERT INTO docs (title, content, collection, metadata, vec_id)
|
|
116
|
+
VALUES (?, ?, ?, ?, ?)""",
|
|
117
|
+
(title, content, collection, json.dumps(metadata), vec_id),
|
|
118
|
+
)
|
|
119
|
+
db.commit()
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
"id": cursor.lastrowid,
|
|
123
|
+
"vec_id": vec_id,
|
|
124
|
+
"title": title,
|
|
125
|
+
"collection": collection,
|
|
126
|
+
"content_length": len(content),
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def turbo_vec_stats():
|
|
131
|
+
"""Estadísticas de la base de conocimiento."""
|
|
132
|
+
db = _ensure_db()
|
|
133
|
+
_, index = _ensure_model()
|
|
134
|
+
|
|
135
|
+
total_docs = db.execute("SELECT COUNT(*) FROM docs").fetchone()[0]
|
|
136
|
+
collections = db.execute(
|
|
137
|
+
"SELECT collection, COUNT(*) as cnt FROM docs GROUP BY collection ORDER BY cnt DESC"
|
|
138
|
+
).fetchall()
|
|
139
|
+
index_size = index.size() if index else 0
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
"total_docs": total_docs,
|
|
143
|
+
"index_size": index_size,
|
|
144
|
+
"collections": [
|
|
145
|
+
{"name": r["collection"], "count": r["cnt"]} for r in collections
|
|
146
|
+
],
|
|
147
|
+
"model": MODEL_NAME,
|
|
148
|
+
"db_path": str(DB_PATH),
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# ── MCP JSON-RPC sobre stdio ────────────────────────────────────────────────
|
|
153
|
+
TOOLS = {
|
|
154
|
+
"turbo_vec_search": turbo_vec_search,
|
|
155
|
+
"turbo_vec_store": turbo_vec_store,
|
|
156
|
+
"turbo_vec_stats": turbo_vec_stats,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
TOOL_SCHEMAS = [
|
|
160
|
+
{
|
|
161
|
+
"name": "turbo_vec_search",
|
|
162
|
+
"description": "Búsqueda semántica en la base de conocimiento TurboVec. Encuentra documentos similares por significado.",
|
|
163
|
+
"inputSchema": {
|
|
164
|
+
"type": "object",
|
|
165
|
+
"properties": {
|
|
166
|
+
"query": {"type": "string", "description": "Texto de búsqueda"},
|
|
167
|
+
"k": {"type": "integer", "description": "Número de resultados (default: 5)", "default": 5},
|
|
168
|
+
"collection": {"type": "string", "description": "Filtrar por colección (opcional)"},
|
|
169
|
+
},
|
|
170
|
+
"required": ["query"],
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"name": "turbo_vec_store",
|
|
175
|
+
"description": "Almacena un documento con embedding semántico en TurboVec.",
|
|
176
|
+
"inputSchema": {
|
|
177
|
+
"type": "object",
|
|
178
|
+
"properties": {
|
|
179
|
+
"title": {"type": "string", "description": "Título del documento"},
|
|
180
|
+
"content": {"type": "string", "description": "Contenido del documento"},
|
|
181
|
+
"collection": {"type": "string", "description": "Colección (default: 'default')", "default": "default"},
|
|
182
|
+
"metadata_json": {"type": "string", "description": "Metadatos en JSON", "default": "{}"},
|
|
183
|
+
},
|
|
184
|
+
"required": ["title", "content"],
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
"name": "turbo_vec_stats",
|
|
189
|
+
"description": "Estadísticas de la base de conocimiento: total de documentos, colecciones, tamaño del índice.",
|
|
190
|
+
"inputSchema": {"type": "object", "properties": {}},
|
|
191
|
+
},
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def handle_request(req: dict) -> dict:
|
|
196
|
+
"""Procesa una petición JSON-RPC."""
|
|
197
|
+
method = req.get("method", "")
|
|
198
|
+
req_id = req.get("id")
|
|
199
|
+
|
|
200
|
+
if method == "initialize":
|
|
201
|
+
return {
|
|
202
|
+
"jsonrpc": "2.0",
|
|
203
|
+
"id": req_id,
|
|
204
|
+
"result": {
|
|
205
|
+
"protocolVersion": "2024-11-05",
|
|
206
|
+
"serverInfo": {
|
|
207
|
+
"name": "turbo-vec-mcp",
|
|
208
|
+
"version": "3.0.0",
|
|
209
|
+
},
|
|
210
|
+
"capabilities": {"tools": {}},
|
|
211
|
+
},
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if method == "tools/list":
|
|
215
|
+
return {
|
|
216
|
+
"jsonrpc": "2.0",
|
|
217
|
+
"id": req_id,
|
|
218
|
+
"result": {"tools": TOOL_SCHEMAS},
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if method == "tools/call":
|
|
222
|
+
tool_name = req.get("params", {}).get("name", "")
|
|
223
|
+
arguments = req.get("params", {}).get("arguments", {})
|
|
224
|
+
if tool_name not in TOOLS:
|
|
225
|
+
return {
|
|
226
|
+
"jsonrpc": "2.0",
|
|
227
|
+
"id": req_id,
|
|
228
|
+
"error": {"code": -32601, "message": f"Tool not found: {tool_name}"},
|
|
229
|
+
}
|
|
230
|
+
try:
|
|
231
|
+
result = TOOLS[tool_name](**arguments)
|
|
232
|
+
return {
|
|
233
|
+
"jsonrpc": "2.0",
|
|
234
|
+
"id": req_id,
|
|
235
|
+
"result": {"content": [{"type": "text", "text": json.dumps(result, ensure_ascii=False)}]},
|
|
236
|
+
}
|
|
237
|
+
except Exception as e:
|
|
238
|
+
return {
|
|
239
|
+
"jsonrpc": "2.0",
|
|
240
|
+
"id": req_id,
|
|
241
|
+
"error": {"code": -32000, "message": str(e)},
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
"jsonrpc": "2.0",
|
|
246
|
+
"id": req_id,
|
|
247
|
+
"error": {"code": -32601, "message": f"Unknown method: {method}"},
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def main():
|
|
252
|
+
"""Loop principal MCP sobre stdio."""
|
|
253
|
+
for line in sys.stdin:
|
|
254
|
+
line = line.strip()
|
|
255
|
+
if not line:
|
|
256
|
+
continue
|
|
257
|
+
try:
|
|
258
|
+
req = json.loads(line)
|
|
259
|
+
resp = handle_request(req)
|
|
260
|
+
sys.stdout.write(json.dumps(resp, ensure_ascii=False) + "\n")
|
|
261
|
+
sys.stdout.flush()
|
|
262
|
+
except json.JSONDecodeError:
|
|
263
|
+
continue
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
if __name__ == "__main__":
|
|
267
|
+
main()
|