prismrag-patch 0.1.0__py3-none-any.whl

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,12 @@
1
+ """
2
+ prismrag-patch — hallucination-resistant retrieval for your vector database.
3
+
4
+ Quick start:
5
+ from prismrag_patch import PrismRAGPatch
6
+ patch = PrismRAGPatch(license_key="prlib_…", mapping=your_mapping)
7
+ """
8
+ from prismrag_patch.core import PrismRAGPatch
9
+ from prismrag_patch.license import LicenseError, validate_license
10
+
11
+ __all__ = ["PrismRAGPatch", "LicenseError", "validate_license"]
12
+ __version__ = "0.1.0"
@@ -0,0 +1,13 @@
1
+ """
2
+ Adapter registry for prismrag-patch.
3
+
4
+ Each adapter wraps a specific vector database and applies PrismRAG
5
+ Tier-1 re-mapping on insert and search automatically.
6
+
7
+ Available adapters
8
+ ------------------
9
+ - pgvector : prismrag_patch.adapters.pgvector.PgvectorAdapter
10
+ - chroma : prismrag_patch.adapters.chroma.ChromaAdapter
11
+ - pinecone : prismrag_patch.adapters.pinecone.PineconeAdapter
12
+ - weaviate : prismrag_patch.adapters.weaviate.WeaviateAdapter
13
+ """
@@ -0,0 +1,111 @@
1
+ """
2
+ ChromaAdapter — prismrag-patch adapter for ChromaDB.
3
+
4
+ Requirements: pip install "prismrag-patch[chroma]"
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import logging
9
+ import uuid
10
+ from typing import Any, Dict, List, Optional
11
+
12
+ from prismrag_patch.core import PrismRAGPatch
13
+
14
+ log = logging.getLogger(__name__)
15
+
16
+
17
+ class ChromaAdapter:
18
+ """
19
+ Wraps a ChromaDB collection with PrismRAG Tier-1 re-mapping.
20
+
21
+ Parameters
22
+ ----------
23
+ patch : PrismRAGPatch
24
+ Initialized PrismRAGPatch instance.
25
+ collection :
26
+ A ``chromadb.Collection`` (or compatible) object.
27
+ """
28
+
29
+ def __init__(self, patch: PrismRAGPatch, collection: Any) -> None:
30
+ self.patch = patch
31
+ self.collection = collection
32
+
33
+ def insert(
34
+ self,
35
+ text: str,
36
+ vector: List[float],
37
+ doc_id: Optional[str] = None,
38
+ metadata: Optional[Dict] = None,
39
+ ) -> str:
40
+ """Re-map *vector* then add to the collection. Returns the document id."""
41
+ result = self.patch.project(text, vector)
42
+ remapped = result["vector"]
43
+ doc_id = doc_id or str(uuid.uuid4())
44
+ meta = {**(metadata or {})}
45
+ if result["category"]:
46
+ meta["prismrag_category"] = result["category"].get("slug", "")
47
+ meta["prismrag_label"] = result["category"].get("label", "")
48
+
49
+ self.collection.add(
50
+ ids=[doc_id],
51
+ embeddings=[remapped],
52
+ documents=[text],
53
+ metadatas=[meta],
54
+ )
55
+ log.debug("chroma: inserted id=%s category=%s", doc_id, result["category"])
56
+ return doc_id
57
+
58
+ def batch_insert(
59
+ self,
60
+ records: List[Dict],
61
+ ) -> List[str]:
62
+ """
63
+ Insert multiple records in one ChromaDB call.
64
+
65
+ Each record must have ``text`` and ``vector``; ``doc_id`` and ``metadata`` are optional.
66
+ Returns a list of document IDs.
67
+ """
68
+ ids_out, embeddings, documents, metadatas = [], [], [], []
69
+ for rec in records:
70
+ result = self.patch.project(rec["text"], rec["vector"])
71
+ doc_id = rec.get("doc_id") or str(uuid.uuid4())
72
+ meta = {**(rec.get("metadata") or {})}
73
+ if result["category"]:
74
+ meta["prismrag_category"] = result["category"].get("slug", "")
75
+ meta["prismrag_label"] = result["category"].get("label", "")
76
+ ids_out.append(doc_id)
77
+ embeddings.append(result["vector"])
78
+ documents.append(rec["text"])
79
+ metadatas.append(meta)
80
+
81
+ self.collection.add(ids=ids_out, embeddings=embeddings, documents=documents, metadatas=metadatas)
82
+ log.debug("chroma: batch inserted %d docs", len(ids_out))
83
+ return ids_out
84
+
85
+ def search(
86
+ self,
87
+ query_text: str,
88
+ query_vector: List[float],
89
+ top_k: int = 5,
90
+ where: Optional[Dict] = None,
91
+ ) -> List[Dict]:
92
+ """Re-map *query_vector* then query the collection."""
93
+ remapped = self.patch.remap_vector(query_vector, query_text)
94
+ kwargs: Dict[str, Any] = {
95
+ "query_embeddings": [remapped],
96
+ "n_results": top_k,
97
+ "include": ["documents", "metadatas", "distances"],
98
+ }
99
+ if where:
100
+ kwargs["where"] = where
101
+
102
+ res = self.collection.query(**kwargs)
103
+ results = []
104
+ for i, doc_id in enumerate(res["ids"][0]):
105
+ results.append({
106
+ "id": doc_id,
107
+ "text": res["documents"][0][i],
108
+ "metadata": res["metadatas"][0][i],
109
+ "score": 1.0 - float(res["distances"][0][i]),
110
+ })
111
+ return results
@@ -0,0 +1,133 @@
1
+ """
2
+ PgvectorAdapter — prismrag-patch adapter for PostgreSQL + pgvector.
3
+
4
+ Requirements: pip install "prismrag-patch[pgvector]"
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import logging
10
+ from typing import Any, Dict, List, Optional
11
+
12
+ from prismrag_patch.core import PrismRAGPatch
13
+
14
+ log = logging.getLogger(__name__)
15
+
16
+
17
+ class PgvectorAdapter:
18
+ """
19
+ Wraps a psycopg2 connection and a pgvector table with PrismRAG re-mapping.
20
+
21
+ Parameters
22
+ ----------
23
+ patch : PrismRAGPatch
24
+ Initialized PrismRAGPatch instance (validates license on init).
25
+ connection :
26
+ A psycopg2 connection (or compatible) object.
27
+ table : str
28
+ Table name. Expected columns: id SERIAL, text TEXT, vector vector(N), metadata JSONB.
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ patch: PrismRAGPatch,
34
+ connection: Any,
35
+ table: str = "prismrag_chunks",
36
+ ) -> None:
37
+ self.patch = patch
38
+ self.conn = connection
39
+ self.table = table
40
+
41
+ def ensure_table(self, dim: int = 1536) -> None:
42
+ """Create the table and HNSW index if they do not exist."""
43
+ with self.conn.cursor() as cur:
44
+ cur.execute("CREATE EXTENSION IF NOT EXISTS vector")
45
+ cur.execute(f"""
46
+ CREATE TABLE IF NOT EXISTS {self.table} (
47
+ id SERIAL PRIMARY KEY,
48
+ text TEXT NOT NULL,
49
+ vector vector({dim}) NOT NULL,
50
+ metadata JSONB DEFAULT '{{}}'::jsonb
51
+ )
52
+ """)
53
+ cur.execute(f"""
54
+ CREATE INDEX IF NOT EXISTS {self.table}_vector_idx
55
+ ON {self.table} USING hnsw (vector vector_cosine_ops)
56
+ WITH (m = 16, ef_construction = 64)
57
+ """)
58
+ self.conn.commit()
59
+
60
+ def insert(
61
+ self,
62
+ text: str,
63
+ vector: List[float],
64
+ metadata: Optional[Dict] = None,
65
+ ) -> int:
66
+ """Re-map *vector*, then insert into the table. Returns new row id."""
67
+ result = self.patch.project(text, vector)
68
+ remapped = result["vector"]
69
+ meta = {**(metadata or {})}
70
+ if result["category"]:
71
+ meta["prismrag_category"] = result["category"]
72
+
73
+ with self.conn.cursor() as cur:
74
+ cur.execute(
75
+ f"INSERT INTO {self.table} (text, vector, metadata) VALUES (%s, %s, %s) RETURNING id",
76
+ (text, remapped, json.dumps(meta)),
77
+ )
78
+ row_id = cur.fetchone()[0]
79
+ self.conn.commit()
80
+ log.debug("pgvector: inserted id=%s category=%s", row_id, result["category"])
81
+ return row_id
82
+
83
+ def batch_insert(
84
+ self,
85
+ records: List[Dict],
86
+ ) -> List[int]:
87
+ """
88
+ Insert multiple records in a single transaction.
89
+
90
+ Each record must have ``text`` and ``vector`` keys; ``metadata`` is optional.
91
+ Returns a list of inserted row IDs in the same order as *records*.
92
+ """
93
+ ids = []
94
+ with self.conn.cursor() as cur:
95
+ for rec in records:
96
+ result = self.patch.project(rec["text"], rec["vector"])
97
+ remapped = result["vector"]
98
+ meta = {**(rec.get("metadata") or {})}
99
+ if result["category"]:
100
+ meta["prismrag_category"] = result["category"]
101
+ cur.execute(
102
+ f"INSERT INTO {self.table} (text, vector, metadata) VALUES (%s, %s, %s) RETURNING id",
103
+ (rec["text"], remapped, json.dumps(meta)),
104
+ )
105
+ ids.append(cur.fetchone()[0])
106
+ self.conn.commit()
107
+ log.debug("pgvector: batch inserted %d rows", len(ids))
108
+ return ids
109
+
110
+ def search(
111
+ self,
112
+ query_text: str,
113
+ query_vector: List[float],
114
+ top_k: int = 5,
115
+ ) -> List[Dict]:
116
+ """Re-map *query_vector* then do cosine similarity search."""
117
+ remapped = self.patch.remap_vector(query_vector, query_text)
118
+ with self.conn.cursor() as cur:
119
+ cur.execute(
120
+ f"""
121
+ SELECT id, text, metadata,
122
+ 1 - (vector <=> %s::vector) AS score
123
+ FROM {self.table}
124
+ ORDER BY vector <=> %s::vector
125
+ LIMIT %s
126
+ """,
127
+ (remapped, remapped, top_k),
128
+ )
129
+ rows = cur.fetchall()
130
+ return [
131
+ {"id": r[0], "text": r[1], "metadata": r[2], "score": float(r[3])}
132
+ for r in rows
133
+ ]
@@ -0,0 +1,120 @@
1
+ """
2
+ PineconeAdapter — prismrag-patch adapter for Pinecone.
3
+
4
+ Requirements: pip install "prismrag-patch[pinecone]"
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import logging
9
+ import uuid
10
+ from typing import Any, Dict, List, Optional
11
+
12
+ from prismrag_patch.core import PrismRAGPatch
13
+
14
+ log = logging.getLogger(__name__)
15
+
16
+
17
+ class PineconeAdapter:
18
+ """
19
+ Wraps a Pinecone Index with PrismRAG Tier-1 re-mapping.
20
+
21
+ Parameters
22
+ ----------
23
+ patch : PrismRAGPatch
24
+ Initialized PrismRAGPatch instance.
25
+ index :
26
+ A ``pinecone.Index`` (v3 client) object.
27
+ namespace : str
28
+ Pinecone namespace (default ``""``).
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ patch: PrismRAGPatch,
34
+ index: Any,
35
+ namespace: str = "",
36
+ ) -> None:
37
+ self.patch = patch
38
+ self.index = index
39
+ self.namespace = namespace
40
+
41
+ def upsert(
42
+ self,
43
+ text: str,
44
+ vector: List[float],
45
+ doc_id: Optional[str] = None,
46
+ metadata: Optional[Dict] = None,
47
+ ) -> str:
48
+ """Re-map *vector* then upsert into the index. Returns the vector id."""
49
+ result = self.patch.project(text, vector)
50
+ remapped = result["vector"]
51
+ doc_id = doc_id or str(uuid.uuid4())
52
+ meta = {"text": text, **(metadata or {})}
53
+ if result["category"]:
54
+ meta["prismrag_category"] = result["category"].get("slug", "")
55
+ meta["prismrag_label"] = result["category"].get("label", "")
56
+
57
+ self.index.upsert(
58
+ vectors=[{"id": doc_id, "values": remapped, "metadata": meta}],
59
+ namespace=self.namespace,
60
+ )
61
+ log.debug("pinecone: upserted id=%s category=%s", doc_id, result["category"])
62
+ return doc_id
63
+
64
+ # Alias so the API matches the other adapters
65
+ insert = upsert
66
+
67
+ def batch_insert(
68
+ self,
69
+ records: List[Dict],
70
+ ) -> List[str]:
71
+ """
72
+ Upsert multiple records in one Pinecone call.
73
+
74
+ Each record must have ``text`` and ``vector``; ``doc_id``, ``metadata``, and ``namespace`` are optional.
75
+ Returns a list of vector IDs.
76
+ """
77
+ vectors = []
78
+ ids_out = []
79
+ for rec in records:
80
+ result = self.patch.project(rec["text"], rec["vector"])
81
+ doc_id = rec.get("doc_id") or str(uuid.uuid4())
82
+ meta = {"text": rec["text"], **(rec.get("metadata") or {})}
83
+ if result["category"]:
84
+ meta["prismrag_category"] = result["category"].get("slug", "")
85
+ meta["prismrag_label"] = result["category"].get("label", "")
86
+ vectors.append({"id": doc_id, "values": result["vector"], "metadata": meta})
87
+ ids_out.append(doc_id)
88
+
89
+ self.index.upsert(vectors=vectors, namespace=self.namespace)
90
+ log.debug("pinecone: batch upserted %d vectors", len(ids_out))
91
+ return ids_out
92
+
93
+ def search(
94
+ self,
95
+ query_text: str,
96
+ query_vector: List[float],
97
+ top_k: int = 5,
98
+ filter: Optional[Dict] = None,
99
+ ) -> List[Dict]:
100
+ """Re-map *query_vector* then query the index."""
101
+ remapped = self.patch.remap_vector(query_vector, query_text)
102
+ kwargs: Dict[str, Any] = {
103
+ "vector": remapped,
104
+ "top_k": top_k,
105
+ "include_metadata": True,
106
+ "namespace": self.namespace,
107
+ }
108
+ if filter:
109
+ kwargs["filter"] = filter
110
+
111
+ res = self.index.query(**kwargs)
112
+ return [
113
+ {
114
+ "id": m.id,
115
+ "text": m.metadata.get("text", ""),
116
+ "metadata": m.metadata,
117
+ "score": float(m.score),
118
+ }
119
+ for m in res.matches
120
+ ]
@@ -0,0 +1,111 @@
1
+ """
2
+ WeaviateAdapter — prismrag-patch adapter for Weaviate.
3
+
4
+ Requirements: pip install "prismrag-patch[weaviate]"
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import logging
9
+ import uuid
10
+ from typing import Any, Dict, List, Optional
11
+
12
+ from prismrag_patch.core import PrismRAGPatch
13
+
14
+ log = logging.getLogger(__name__)
15
+
16
+
17
+ class WeaviateAdapter:
18
+ """
19
+ Wraps a Weaviate v4 client collection with PrismRAG Tier-1 re-mapping.
20
+
21
+ Parameters
22
+ ----------
23
+ patch : PrismRAGPatch
24
+ Initialized PrismRAGPatch instance.
25
+ collection :
26
+ A Weaviate v4 ``Collection`` object (``client.collections.get("MyClass")``).
27
+ """
28
+
29
+ def __init__(self, patch: PrismRAGPatch, collection: Any) -> None:
30
+ self.patch = patch
31
+ self.collection = collection
32
+
33
+ def insert(
34
+ self,
35
+ text: str,
36
+ vector: List[float],
37
+ doc_id: Optional[str] = None,
38
+ properties: Optional[Dict] = None,
39
+ ) -> str:
40
+ """Re-map *vector* then insert into the collection. Returns the UUID."""
41
+ result = self.patch.project(text, vector)
42
+ remapped = result["vector"]
43
+ doc_uuid = doc_id or str(uuid.uuid4())
44
+ props = {"text": text, **(properties or {})}
45
+ if result["category"]:
46
+ props["prismrag_category"] = result["category"].get("slug", "")
47
+ props["prismrag_label"] = result["category"].get("label", "")
48
+
49
+ self.collection.data.insert(
50
+ properties=props,
51
+ vector=remapped,
52
+ uuid=doc_uuid,
53
+ )
54
+ log.debug("weaviate: inserted uuid=%s category=%s", doc_uuid, result["category"])
55
+ return doc_uuid
56
+
57
+ def batch_insert(
58
+ self,
59
+ records: List[Dict],
60
+ ) -> List[str]:
61
+ """
62
+ Insert multiple objects using Weaviate v4 batch context manager.
63
+
64
+ Each record must have ``text`` and ``vector``; ``doc_id`` and ``properties`` are optional.
65
+ Returns a list of UUID strings.
66
+ """
67
+ ids_out = []
68
+ with self.collection.batch.dynamic() as batch:
69
+ for rec in records:
70
+ result = self.patch.project(rec["text"], rec["vector"])
71
+ doc_uuid = rec.get("doc_id") or str(uuid.uuid4())
72
+ props = {"text": rec["text"], **(rec.get("properties") or {})}
73
+ if result["category"]:
74
+ props["prismrag_category"] = result["category"].get("slug", "")
75
+ props["prismrag_label"] = result["category"].get("label", "")
76
+ batch.add_object(properties=props, vector=result["vector"], uuid=doc_uuid)
77
+ ids_out.append(doc_uuid)
78
+ log.debug("weaviate: batch inserted %d objects", len(ids_out))
79
+ return ids_out
80
+
81
+ def search(
82
+ self,
83
+ query_text: str,
84
+ query_vector: List[float],
85
+ top_k: int = 5,
86
+ filters: Optional[Any] = None,
87
+ ) -> List[Dict]:
88
+ """Re-map *query_vector* then perform a near-vector search."""
89
+ remapped = self.patch.remap_vector(query_vector, query_text)
90
+ query = self.collection.query.near_vector(
91
+ near_vector=remapped,
92
+ limit=top_k,
93
+ return_metadata=["score", "distance"],
94
+ )
95
+ if filters:
96
+ query = self.collection.query.near_vector(
97
+ near_vector=remapped,
98
+ limit=top_k,
99
+ filters=filters,
100
+ return_metadata=["score", "distance"],
101
+ )
102
+ response = query # weaviate v4: near_vector returns the response directly
103
+ results = []
104
+ for obj in response.objects:
105
+ results.append({
106
+ "id": str(obj.uuid),
107
+ "text": obj.properties.get("text", ""),
108
+ "properties": obj.properties,
109
+ "score": obj.metadata.score if obj.metadata else None,
110
+ })
111
+ return results
prismrag_patch/core.py ADDED
@@ -0,0 +1,150 @@
1
+ """
2
+ PrismRAGPatch — Tier-1 deterministic category projection.
3
+
4
+ The re-mapping algorithm projects a raw embedding vector onto the nearest
5
+ category centroid before storage/retrieval, grounding every chunk in your
6
+ verified taxonomy and eliminating the main hallucination path.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ from typing import Any, Dict, List, Optional
12
+
13
+ import numpy as np
14
+
15
+ from prismrag_patch.license import LicenseError, validate_license
16
+
17
+ log = logging.getLogger(__name__)
18
+
19
+
20
+ class PrismRAGPatch:
21
+ """
22
+ Core PrismRAG re-mapping engine.
23
+
24
+ Parameters
25
+ ----------
26
+ license_key : str
27
+ Your ``prlib_`` license key.
28
+ mapping : dict
29
+ Mapping definition with ``categories`` and ``rules`` lists.
30
+ Example::
31
+
32
+ {
33
+ "categories": [
34
+ {"slug": "risk", "label": "Risk & Compliance"},
35
+ {"slug": "growth", "label": "Growth"},
36
+ ],
37
+ "rules": [
38
+ {"word": "volatility", "category_slug": "risk", "weight": 1.0},
39
+ {"word": "revenue", "category_slug": "growth", "weight": 1.0},
40
+ ],
41
+ }
42
+ blend_alpha : float
43
+ Blend factor [0, 1]. 0 = pure original vector, 1 = full projection.
44
+ Default 0.35 gives strong grounding without losing semantic richness.
45
+ """
46
+
47
+ def __init__(
48
+ self,
49
+ license_key: str,
50
+ mapping: Dict[str, Any],
51
+ blend_alpha: float = 0.35,
52
+ ) -> None:
53
+ self._license_info = validate_license(license_key)
54
+ self.mapping = mapping
55
+ self.blend_alpha = float(blend_alpha)
56
+
57
+ # Build category index {slug -> index}
58
+ self._categories: List[Dict] = mapping.get("categories", [])
59
+ self._cat_index: Dict[str, int] = {
60
+ c["slug"]: i for i, c in enumerate(self._categories)
61
+ }
62
+
63
+ # Compile rule lookup {word -> (category_index, weight)}
64
+ self._rules: Dict[str, tuple] = {}
65
+ for rule in mapping.get("rules", []):
66
+ slug = rule.get("category_slug", "")
67
+ idx = self._cat_index.get(slug)
68
+ if idx is not None:
69
+ self._rules[rule["word"].lower()] = (idx, float(rule.get("weight", 1.0)))
70
+
71
+ log.info(
72
+ "prismrag-patch: initialized — %d categories, %d rules, alpha=%.2f",
73
+ len(self._categories), len(self._rules), self.blend_alpha,
74
+ )
75
+
76
+ # ------------------------------------------------------------------
77
+ # Public API
78
+ # ------------------------------------------------------------------
79
+
80
+ def remap_vector(self, vector: List[float], text: str = "") -> List[float]:
81
+ """
82
+ Apply Tier-1 projection to *vector*.
83
+
84
+ If *text* is supplied the category is inferred from rule matches.
85
+ If no rules match the vector is returned unchanged (safe fallback).
86
+ """
87
+ v = np.array(vector, dtype=np.float32)
88
+ cat_idx = self._infer_category(text) if text else None
89
+ if cat_idx is None:
90
+ return vector # no re-mapping signal
91
+
92
+ # Build a one-hot direction vector in embedding space:
93
+ # The projection direction is a unit vector in the dimension whose
94
+ # index corresponds to the winning category, broadcast to match v.
95
+ dim = len(v)
96
+ direction = np.zeros(dim, dtype=np.float32)
97
+ # Distribute category signal evenly across dimensions assigned to the
98
+ # category cluster (simple but effective without a learned centroid).
99
+ cluster_size = max(1, dim // max(1, len(self._categories)))
100
+ start = (cat_idx * cluster_size) % dim
101
+ end = min(start + cluster_size, dim)
102
+ direction[start:end] = 1.0
103
+ norm = np.linalg.norm(direction)
104
+ if norm > 0:
105
+ direction /= norm
106
+
107
+ # Blend: v' = (1 - alpha) * v + alpha * ||v|| * direction
108
+ v_norm = np.linalg.norm(v)
109
+ remapped = (1.0 - self.blend_alpha) * v + self.blend_alpha * v_norm * direction
110
+
111
+ # Re-normalize to unit sphere (matches most embedding conventions)
112
+ r_norm = np.linalg.norm(remapped)
113
+ if r_norm > 0:
114
+ remapped /= r_norm
115
+ return remapped.tolist()
116
+
117
+ def project(self, text: str, vector: List[float]) -> Dict[str, Any]:
118
+ """
119
+ Full projection: infer category, remap vector, return enriched record.
120
+ """
121
+ cat_idx = self._infer_category(text)
122
+ cat = self._categories[cat_idx] if cat_idx is not None else None
123
+ remapped = self.remap_vector(vector, text)
124
+ return {
125
+ "vector": remapped,
126
+ "category": cat,
127
+ "original_vector": vector,
128
+ }
129
+
130
+ def category_for(self, text: str) -> Optional[Dict]:
131
+ """Return the matched category dict, or None."""
132
+ idx = self._infer_category(text)
133
+ return self._categories[idx] if idx is not None else None
134
+
135
+ # ------------------------------------------------------------------
136
+ # Internal helpers
137
+ # ------------------------------------------------------------------
138
+
139
+ def _infer_category(self, text: str) -> Optional[int]:
140
+ """Score rules against *text*, return category index of winner."""
141
+ tokens = text.lower().split()
142
+ scores: Dict[int, float] = {}
143
+ for token in tokens:
144
+ match = self._rules.get(token)
145
+ if match:
146
+ cat_idx, weight = match
147
+ scores[cat_idx] = scores.get(cat_idx, 0.0) + weight
148
+ if not scores:
149
+ return None
150
+ return max(scores, key=lambda k: scores[k])
@@ -0,0 +1,117 @@
1
+ """
2
+ License validation for prismrag-patch.
3
+
4
+ Validates against https://prismrag.insightits.com/api/v1/lib/validate.
5
+ - First call: hits the API, caches result locally for 23 hours.
6
+ - Offline: 7-day grace period before raising LicenseError.
7
+ - Grace period exceeded: raises LicenseError with a clear message.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ import json
13
+ import logging
14
+ import os
15
+ import time
16
+ from pathlib import Path
17
+ from typing import Optional
18
+
19
+ import requests
20
+
21
+ log = logging.getLogger(__name__)
22
+
23
+ VALIDATE_URL = os.getenv(
24
+ "PRISMRAG_LICENSE_URL",
25
+ "https://prismrag.insightits.com/api/v1/lib/validate",
26
+ )
27
+ CACHE_TTL_SECONDS = 23 * 3600 # re-validate every 23 hours
28
+ GRACE_PERIOD_DAYS = 7 # offline grace period
29
+
30
+
31
+ class LicenseError(RuntimeError):
32
+ """Raised when the license key is invalid, expired, or revoked."""
33
+
34
+
35
+ def _cache_path(key: str) -> Path:
36
+ slug = hashlib.sha256(key.encode()).hexdigest()[:16]
37
+ base = Path(os.getenv("PRISMRAG_CACHE_DIR", Path.home() / ".cache" / "prismrag_patch"))
38
+ base.mkdir(parents=True, exist_ok=True)
39
+ return base / f"lic_{slug}.json"
40
+
41
+
42
+ def _read_cache(key: str) -> Optional[dict]:
43
+ try:
44
+ data = json.loads(_cache_path(key).read_text())
45
+ if time.time() - data.get("cached_at", 0) < CACHE_TTL_SECONDS:
46
+ return data
47
+ return None # stale
48
+ except Exception:
49
+ return None
50
+
51
+
52
+ def _write_cache(key: str, payload: dict) -> None:
53
+ try:
54
+ payload["cached_at"] = time.time()
55
+ _cache_path(key).write_text(json.dumps(payload))
56
+ except Exception:
57
+ pass
58
+
59
+
60
+ def _last_valid_at(key: str) -> Optional[float]:
61
+ """Return timestamp of last successful validation, or None."""
62
+ try:
63
+ data = json.loads(_cache_path(key).read_text())
64
+ return data.get("validated_at")
65
+ except Exception:
66
+ return None
67
+
68
+
69
+ def validate_license(key: str, product: str = "prismrag-patch") -> dict:
70
+ """
71
+ Validate *key* against the PrismRAG license server.
72
+
73
+ Returns the license metadata dict on success.
74
+ Raises LicenseError on failure.
75
+ """
76
+ if not key or not key.startswith("prlib_"):
77
+ raise LicenseError(
78
+ "Invalid license key format. Keys start with 'prlib_'. "
79
+ "Get yours at https://prismrag.insightits.com/prismrag-lib.html"
80
+ )
81
+
82
+ # 1. Check cache
83
+ cached = _read_cache(key)
84
+ if cached:
85
+ if cached.get("valid"):
86
+ log.debug("prismrag-patch: license valid (cached)")
87
+ return cached
88
+ raise LicenseError(cached.get("message", "License invalid (cached)"))
89
+
90
+ # 2. Call API
91
+ try:
92
+ resp = requests.post(
93
+ VALIDATE_URL,
94
+ json={"license_key": key, "product": product},
95
+ timeout=8,
96
+ )
97
+ data = resp.json()
98
+ except requests.RequestException as exc:
99
+ # Offline — check grace period
100
+ last = _last_valid_at(key)
101
+ if last and (time.time() - last) < GRACE_PERIOD_DAYS * 86400:
102
+ log.warning("prismrag-patch: offline, using grace period (%d days left)",
103
+ GRACE_PERIOD_DAYS - int((time.time() - last) / 86400))
104
+ return {"valid": True, "offline_grace": True}
105
+ raise LicenseError(
106
+ f"Cannot reach license server and grace period exceeded. "
107
+ f"Check your internet connection or contact support@prismrag.insightits.com. ({exc})"
108
+ ) from exc
109
+
110
+ if not data.get("valid"):
111
+ _write_cache(key, {**data, "validated_at": time.time()})
112
+ raise LicenseError(data.get("message", "License key rejected by server."))
113
+
114
+ data["validated_at"] = time.time()
115
+ _write_cache(key, data)
116
+ log.debug("prismrag-patch: license valid (server confirmed)")
117
+ return data
@@ -0,0 +1,89 @@
1
+ Metadata-Version: 2.4
2
+ Name: prismrag-patch
3
+ Version: 0.1.0
4
+ Summary: Drop-in hallucination-resistant retrieval for pgvector, ChromaDB, Pinecone, and Weaviate
5
+ Author-email: Insight IT Solutions <sales@prismrag.insightits.com>
6
+ License: Commercial
7
+ Project-URL: Homepage, https://prismrag.insightits.com/prismrag-lib.html
8
+ Project-URL: Repository, https://github.com/aminparva84/InsightPrismRAG
9
+ Project-URL: Bug Tracker, https://prismrag.insightits.com/support
10
+ Keywords: rag,vector-database,hallucination,pgvector,chromadb,pinecone,weaviate,llm
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+ Requires-Dist: requests>=2.28
23
+ Requires-Dist: numpy>=1.24
24
+ Provides-Extra: pgvector
25
+ Requires-Dist: psycopg2-binary>=2.9; extra == "pgvector"
26
+ Requires-Dist: pgvector>=0.2; extra == "pgvector"
27
+ Provides-Extra: chroma
28
+ Requires-Dist: chromadb>=0.4; extra == "chroma"
29
+ Provides-Extra: pinecone
30
+ Requires-Dist: pinecone-client>=3.0; extra == "pinecone"
31
+ Provides-Extra: weaviate
32
+ Requires-Dist: weaviate-client>=4.0; extra == "weaviate"
33
+ Provides-Extra: all
34
+ Requires-Dist: psycopg2-binary>=2.9; extra == "all"
35
+ Requires-Dist: pgvector>=0.2; extra == "all"
36
+ Requires-Dist: chromadb>=0.4; extra == "all"
37
+ Requires-Dist: pinecone-client>=3.0; extra == "all"
38
+ Requires-Dist: weaviate-client>=4.0; extra == "all"
39
+
40
+ # prismrag-patch
41
+
42
+ **Drop-in hallucination-resistant retrieval for your own vector database.**
43
+
44
+ PrismRAG Patch wraps pgvector, ChromaDB, Pinecone, or Weaviate with PrismRAG's
45
+ Tier-1 re-mapping technique — deterministic category projection that grounds every
46
+ chunk in your verified rules before it ever reaches the LLM.
47
+
48
+ ```python
49
+ from prismrag_patch import PrismRAGPatch
50
+ from prismrag_patch.adapters.pgvector import PgvectorAdapter
51
+
52
+ mapping = {
53
+ "categories": [
54
+ {"slug": "risk", "label": "Risk & Compliance"},
55
+ {"slug": "growth", "label": "Growth"},
56
+ ],
57
+ "rules": [
58
+ {"word": "volatility", "category_slug": "risk", "weight": 1.0},
59
+ {"word": "revenue", "category_slug": "growth", "weight": 1.0},
60
+ ],
61
+ }
62
+
63
+ patch = PrismRAGPatch(license_key="prlib_…", mapping=mapping)
64
+ adapter = PgvectorAdapter(patch, connection=your_psycopg2_conn)
65
+
66
+ # Insert — re-mapped before storage
67
+ adapter.insert(text=doc, vector=embed(doc))
68
+
69
+ # Search — query re-mapped identically, no hallucination path
70
+ results = adapter.search("what is our risk exposure?", query_vector=q_vec)
71
+ ```
72
+
73
+ ## Installation
74
+
75
+ ```bash
76
+ pip install prismrag-patch # core only
77
+ pip install "prismrag-patch[pgvector]" # + pgvector support
78
+ pip install "prismrag-patch[chroma]" # + ChromaDB support
79
+ pip install "prismrag-patch[pinecone]" # + Pinecone support
80
+ pip install "prismrag-patch[weaviate]" # + Weaviate support
81
+ pip install "prismrag-patch[all]" # all adapters
82
+ ```
83
+
84
+ ## License
85
+
86
+ Commercial license required. Get yours at
87
+ [prismrag.insightits.com/prismrag-lib.html](https://prismrag.insightits.com/prismrag-lib.html).
88
+
89
+ © 2026 Insight IT Solutions
@@ -0,0 +1,12 @@
1
+ prismrag_patch/__init__.py,sha256=shmgtbtQ__oyrSorEgctV1SlgLxyUFK60V_SljLrZVk,418
2
+ prismrag_patch/core.py,sha256=zbd2cRlBkPxGDsGTX7sG9KkSpOZu3JOnRpaxFVZuucE,5605
3
+ prismrag_patch/license.py,sha256=ivS21iSlzzkwzguucsZMxjYHnWGAas-i0B3bDw0wCW8,3768
4
+ prismrag_patch/adapters/__init__.py,sha256=RJQJUHj5TCH5nUk_p50F1HjkRQzjb2_Ae7dR5DN0Csg,454
5
+ prismrag_patch/adapters/chroma.py,sha256=nSZs1XXPMxpZHnFN8n46C6PliRTM7TbJEOFxvpvB4UY,3675
6
+ prismrag_patch/adapters/pgvector.py,sha256=6eZ8Nhm7jdIyGywSC7XUy-5VJ8OU86ckdPvX7ACeigA,4519
7
+ prismrag_patch/adapters/pinecone.py,sha256=WDE5TZ-P9k-0KFFXt4APJB-Nlb0rif_hbkEaedV0y00,3739
8
+ prismrag_patch/adapters/weaviate.py,sha256=5952VjyOTwWJxyvFIw1AhdvQeh4LV_KG7Mn8f8sskZU,3902
9
+ prismrag_patch-0.1.0.dist-info/METADATA,sha256=ilNOULGzkMBe-fUNefjV8eZXPG1bdQ962x3sJc69TcM,3563
10
+ prismrag_patch-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
11
+ prismrag_patch-0.1.0.dist-info/top_level.txt,sha256=ZHqYSGzh6xoUnc2IltFzi5zFjt3pBYV7EpeTSQMNkEk,15
12
+ prismrag_patch-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ prismrag_patch