AI-that-works commited on
Commit
6d74c84
Β·
verified Β·
1 Parent(s): 65d8577

Upload 4 files

Browse files
Files changed (4) hide show
  1. Dockerfile +14 -0
  2. README.md +129 -0
  3. app.py +269 -0
  4. requirements.txt +5 -0
Dockerfile ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install dependencies first (layer caching)
6
+ COPY requirements.txt .
7
+ RUN pip install --no-cache-dir -r requirements.txt
8
+
9
+ COPY . .
10
+
11
+ # HF Spaces expects port 7860
12
+ EXPOSE 7860
13
+
14
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
README.md ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: groundlens API
3
+ emoji: πŸ“
4
+ colorFrom: yellow
5
+ colorTo: red
6
+ sdk: docker
7
+ pinned: false
8
+ license: mit
9
+ tags:
10
+ - hallucination-detection
11
+ - llm-evaluation
12
+ - rag
13
+ - grounding
14
+ - groundlens
15
+ - api
16
+ short_description: REST API for geometric LLM hallucination detection
17
+ ---
18
+
19
+ # groundlens API
20
+
21
+ REST API for [groundlens](https://groundlens.dev) β€” LLM hallucination detection using embedding geometry.
22
+
23
+ No second LLM. Deterministic. Same inputs β†’ same scores.
24
+
25
+ ## Endpoints
26
+
27
+ | Method | Path | Description |
28
+ |--------|------|-------------|
29
+ | `POST` | `/v1/check` | Auto-selects SGI or DGI based on context |
30
+ | `POST` | `/v1/sgi` | Context-based grounding check |
31
+ | `POST` | `/v1/dgi` | Context-free grounding check |
32
+ | `GET` | `/health` | Liveness + model status |
33
+ | `GET` | `/docs` | Interactive Swagger UI |
34
+
35
+ ## Quick start
36
+
37
+ ### Check without context (DGI)
38
+
39
+ ```bash
40
+ curl -X POST https://groundlens-groundlens-api.hf.space/v1/check \
41
+ -H "Content-Type: application/json" \
42
+ -d '{
43
+ "question": "What is the capital of France?",
44
+ "response": "The capital of France is Paris."
45
+ }'
46
+ ```
47
+
48
+ ### Check with context (SGI)
49
+
50
+ ```bash
51
+ curl -X POST https://groundlens-groundlens-api.hf.space/v1/check \
52
+ -H "Content-Type: application/json" \
53
+ -d '{
54
+ "question": "What does our policy cover?",
55
+ "response": "The policy covers fire, flood, and theft damage to residential properties.",
56
+ "context": "HomeShield Insurance Policy: Coverage includes damage from fire, flood, and theft for residential properties within the continental United States."
57
+ }'
58
+ ```
59
+
60
+ ### Python
61
+
62
+ ```python
63
+ import requests
64
+
65
+ r = requests.post(
66
+ "https://groundlens-groundlens-api.hf.space/v1/check",
67
+ json={
68
+ "question": "What is the capital of France?",
69
+ "response": "The capital of France is Paris.",
70
+ },
71
+ )
72
+ print(r.json()["verdict"]) # GROUNDED
73
+ ```
74
+
75
+ ### JavaScript
76
+
77
+ ```javascript
78
+ const res = await fetch("https://groundlens-groundlens-api.hf.space/v1/check", {
79
+ method: "POST",
80
+ headers: { "Content-Type": "application/json" },
81
+ body: JSON.stringify({
82
+ question: "What is the capital of France?",
83
+ response: "The capital of France is Paris.",
84
+ }),
85
+ });
86
+ const data = await res.json();
87
+ console.log(data.verdict); // GROUNDED
88
+ ```
89
+
90
+ ## Response format
91
+
92
+ ```json
93
+ {
94
+ "verdict": "GROUNDED",
95
+ "flagged": false,
96
+ "method": "DGI (Directional Grounding Index)",
97
+ "score": 0.4521,
98
+ "threshold": 0.30,
99
+ "explanation": "The response follows patterns typical of grounded answers.",
100
+ "detail": {
101
+ "interpretation": "Positive directional alignment with grounded response patterns."
102
+ },
103
+ "latency_ms": 45
104
+ }
105
+ ```
106
+
107
+ ## Self-hosting
108
+
109
+ ```bash
110
+ git clone https://github.com/groundlens-dev/groundlens-api.git
111
+ cd groundlens-api
112
+ pip install -r requirements.txt
113
+ uvicorn app:app --host 0.0.0.0 --port 8000
114
+ ```
115
+
116
+ Or with Docker:
117
+
118
+ ```bash
119
+ docker build -t groundlens-api .
120
+ docker run -p 8000:7860 groundlens-api
121
+ ```
122
+
123
+ ## Links
124
+
125
+ - [groundlens library](https://github.com/groundlens-dev/groundlens) β€” `pip install groundlens`
126
+ - [MCP Server](https://github.com/groundlens-dev/groundlens-mcp) β€” for Claude Desktop, Cursor, Windsurf
127
+ - [Demo](https://huggingface.co/spaces/groundlens/groundlens-demo) β€” interactive web UI
128
+ - [Documentation](https://docs.groundlens.dev)
129
+ - [Website](https://groundlens.dev)
app.py ADDED
@@ -0,0 +1,269 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ groundlens REST API
3
+
4
+ Lightweight HTTP wrapper around the groundlens library.
5
+ Deploy on Hugging Face Spaces (Docker SDK), Railway, Fly.io, or any container host.
6
+
7
+ Endpoints:
8
+ POST /v1/check β€” auto-selects SGI or DGI based on whether context is provided
9
+ POST /v1/sgi β€” explicit context-based grounding check
10
+ POST /v1/dgi β€” explicit context-free grounding check
11
+ GET /health β€” liveness + model status
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import time
17
+ from contextlib import asynccontextmanager
18
+ from typing import Optional
19
+
20
+ from fastapi import FastAPI, HTTPException
21
+ from fastapi.middleware.cors import CORSMiddleware
22
+ from pydantic import BaseModel, Field, ConfigDict
23
+
24
+ # ─────────────────────────────────────────────────────────────────────────────
25
+ # Model preloading
26
+ # ─────────────────────────────────────────────────────────────────────────────
27
+
28
+ _model_ready = False
29
+ _model_load_time: float = 0.0
30
+
31
+
32
+ def _load_model() -> None:
33
+ """Import groundlens to trigger model download + warm the embedding cache."""
34
+ global _model_ready, _model_load_time
35
+ if _model_ready:
36
+ return
37
+ t0 = time.monotonic()
38
+ from groundlens import compute_dgi # noqa: F401
39
+
40
+ # Warm up β€” first call loads the sentence-transformer model
41
+ compute_dgi(question="warmup", response="warmup")
42
+ _model_load_time = round(time.monotonic() - t0, 2)
43
+ _model_ready = True
44
+
45
+
46
+ @asynccontextmanager
47
+ async def lifespan(app: FastAPI):
48
+ """Load model at startup so first request is fast."""
49
+ _load_model()
50
+ yield
51
+
52
+
53
+ # ─────────────────────────────────────────────────────────────────────────────
54
+ # App
55
+ # ─────────────────────────────────────────────────────────────────────────────
56
+
57
+ app = FastAPI(
58
+ title="groundlens API",
59
+ description=(
60
+ "LLM hallucination detection using embedding geometry. "
61
+ "No second LLM. Deterministic. Same inputs β†’ same scores."
62
+ ),
63
+ version="2026.5.12",
64
+ docs_url="/docs",
65
+ redoc_url="/redoc",
66
+ lifespan=lifespan,
67
+ )
68
+
69
+ app.add_middleware(
70
+ CORSMiddleware,
71
+ allow_origins=["*"],
72
+ allow_credentials=False,
73
+ allow_methods=["GET", "POST", "OPTIONS"],
74
+ allow_headers=["*"],
75
+ )
76
+
77
+
78
+ # ─────────────────────────────────────────────────────────────────────────────
79
+ # Request / Response models
80
+ # ─────────────────────────────────────────────────────────────────────────────
81
+
82
+ class CheckRequest(BaseModel):
83
+ """Auto-select SGI or DGI based on whether context is provided."""
84
+
85
+ model_config = ConfigDict(str_strip_whitespace=True)
86
+
87
+ question: str = Field(
88
+ ...,
89
+ description="The question asked to the LLM",
90
+ min_length=1,
91
+ max_length=10_000,
92
+ )
93
+ response: str = Field(
94
+ ...,
95
+ description="The LLM's response to evaluate",
96
+ min_length=1,
97
+ max_length=50_000,
98
+ )
99
+ context: Optional[str] = Field(
100
+ default=None,
101
+ description=(
102
+ "Source material (document, RAG chunks, reference text). "
103
+ "If provided β†’ SGI. If omitted β†’ DGI."
104
+ ),
105
+ max_length=100_000,
106
+ )
107
+
108
+
109
+ class SGIRequest(BaseModel):
110
+ """Explicit context-based grounding check."""
111
+
112
+ model_config = ConfigDict(str_strip_whitespace=True)
113
+
114
+ question: str = Field(..., min_length=1, max_length=10_000)
115
+ context: str = Field(..., min_length=1, max_length=100_000)
116
+ response: str = Field(..., min_length=1, max_length=50_000)
117
+
118
+
119
+ class DGIRequest(BaseModel):
120
+ """Explicit context-free grounding check."""
121
+
122
+ model_config = ConfigDict(str_strip_whitespace=True)
123
+
124
+ question: str = Field(..., min_length=1, max_length=10_000)
125
+ response: str = Field(..., min_length=1, max_length=50_000)
126
+
127
+
128
+ class SGIDetail(BaseModel):
129
+ q_dist: float
130
+ ctx_dist: float
131
+ interpretation: str
132
+
133
+
134
+ class DGIDetail(BaseModel):
135
+ interpretation: str
136
+
137
+
138
+ class GroundingResult(BaseModel):
139
+ verdict: str = Field(description="GROUNDED or HALLUCINATION RISK")
140
+ flagged: bool = Field(description="True if hallucination risk detected")
141
+ method: str = Field(description="SGI or DGI")
142
+ score: float = Field(description="Grounding score")
143
+ threshold: float = Field(description="Score threshold for flagging")
144
+ explanation: str = Field(description="Plain-language explanation")
145
+ detail: SGIDetail | DGIDetail
146
+ latency_ms: int = Field(description="Processing time in milliseconds")
147
+
148
+
149
+ class HealthResponse(BaseModel):
150
+ status: str
151
+ model_loaded: bool
152
+ model_load_time_s: float
153
+ version: str
154
+
155
+
156
+ # ─────────────────────────────────────────────────────────────────────────────
157
+ # Helpers
158
+ # ─────────────────────────────────────────────────────────────────────────────
159
+
160
+ def _run_sgi(question: str, context: str, response: str) -> GroundingResult:
161
+ from groundlens import compute_sgi
162
+
163
+ t0 = time.monotonic()
164
+ result = compute_sgi(question=question, context=context, response=response)
165
+ latency = int((time.monotonic() - t0) * 1000)
166
+
167
+ return GroundingResult(
168
+ verdict="GROUNDED" if not result.flagged else "HALLUCINATION RISK",
169
+ flagged=result.flagged,
170
+ method="SGI (Semantic Grounding Index)",
171
+ score=round(result.value, 4),
172
+ threshold=0.95,
173
+ explanation=(
174
+ "The response appears grounded in the source material."
175
+ if not result.flagged
176
+ else "The response may not be based on the source material provided."
177
+ ),
178
+ detail=SGIDetail(
179
+ q_dist=round(result.q_dist, 4),
180
+ ctx_dist=round(result.ctx_dist, 4),
181
+ interpretation=result.explanation,
182
+ ),
183
+ latency_ms=latency,
184
+ )
185
+
186
+
187
+ def _run_dgi(question: str, response: str) -> GroundingResult:
188
+ from groundlens import compute_dgi
189
+
190
+ t0 = time.monotonic()
191
+ result = compute_dgi(question=question, response=response)
192
+ latency = int((time.monotonic() - t0) * 1000)
193
+
194
+ return GroundingResult(
195
+ verdict="GROUNDED" if not result.flagged else "HALLUCINATION RISK",
196
+ flagged=result.flagged,
197
+ method="DGI (Directional Grounding Index)",
198
+ score=round(result.value, 4),
199
+ threshold=0.30,
200
+ explanation=(
201
+ "The response follows patterns typical of grounded answers."
202
+ if not result.flagged
203
+ else "The response shows geometric patterns associated with hallucination."
204
+ ),
205
+ detail=DGIDetail(
206
+ interpretation=result.explanation,
207
+ ),
208
+ latency_ms=latency,
209
+ )
210
+
211
+
212
+ # ─────────────────────────────────────────────────────────────────────────────
213
+ # Endpoints
214
+ # ─────────────────────────────────────────────────────────────────────────────
215
+
216
+ @app.get("/health", response_model=HealthResponse, tags=["system"])
217
+ async def health():
218
+ """Liveness check. Returns model load status."""
219
+ return HealthResponse(
220
+ status="ok" if _model_ready else "loading",
221
+ model_loaded=_model_ready,
222
+ model_load_time_s=_model_load_time,
223
+ version="2026.5.12",
224
+ )
225
+
226
+
227
+ @app.post("/v1/check", response_model=GroundingResult, tags=["grounding"])
228
+ async def check(req: CheckRequest):
229
+ """Check whether an LLM response is hallucinated.
230
+
231
+ Auto-selects the right method:
232
+ - Context provided β†’ SGI (checks if the response used the source material)
233
+ - No context β†’ DGI (checks geometric grounding patterns)
234
+ """
235
+ if not _model_ready:
236
+ raise HTTPException(503, "Model is still loading. Try again in a few seconds.")
237
+
238
+ has_context = req.context is not None and req.context.strip() != ""
239
+
240
+ if has_context:
241
+ return _run_sgi(req.question, req.context, req.response)
242
+ else:
243
+ return _run_dgi(req.question, req.response)
244
+
245
+
246
+ @app.post("/v1/sgi", response_model=GroundingResult, tags=["grounding"])
247
+ async def sgi(req: SGIRequest):
248
+ """SGI β€” check if the response is grounded in a source document.
249
+
250
+ Use for RAG pipelines, document Q&A, or any case where you have
251
+ the source material the LLM was given.
252
+ """
253
+ if not _model_ready:
254
+ raise HTTPException(503, "Model is still loading. Try again in a few seconds.")
255
+
256
+ return _run_sgi(req.question, req.context, req.response)
257
+
258
+
259
+ @app.post("/v1/dgi", response_model=GroundingResult, tags=["grounding"])
260
+ async def dgi(req: DGIRequest):
261
+ """DGI β€” check grounding patterns without source context.
262
+
263
+ Use for open-ended chat, general Q&A, or any case where you just
264
+ have a question and the LLM's answer.
265
+ """
266
+ if not _model_ready:
267
+ raise HTTPException(503, "Model is still loading. Try again in a few seconds.")
268
+
269
+ return _run_dgi(req.question, req.response)
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ groundlens>=2026.4.0
2
+ fastapi>=0.115.0
3
+ uvicorn[standard]>=0.30.0
4
+ transformers>=4.40.0,<5.0.0
5
+ torch>=2.0.0