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.
@@ -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()