Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- api.py +185 -35
- make_pdf.py +22 -0
- server.log +8 -0
- test_api_client.py +29 -0
- test_gazette.pdf +0 -0
api.py
CHANGED
|
@@ -4,6 +4,10 @@ import hashlib
|
|
| 4 |
import re
|
| 5 |
from datetime import datetime
|
| 6 |
from typing import Optional, Any, List, Dict, cast, AsyncGenerator
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
from fastapi import FastAPI, Request
|
| 8 |
from fastapi.responses import StreamingResponse
|
| 9 |
from pydantic import BaseModel, Field, field_validator
|
|
@@ -26,6 +30,20 @@ from config import settings
|
|
| 26 |
from indra_engine import IndraProjectionEngine
|
| 27 |
indra_engine = IndraProjectionEngine(batch_size=400, dimensions=768)
|
| 28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
# --- SECURE KEYS ---
|
| 30 |
SUPABASE_URL = settings.SUPABASE_URL
|
| 31 |
SUPABASE_KEY = settings.SUPABASE_KEY
|
|
@@ -35,7 +53,17 @@ ADMIN_SECRET = settings.ADMIN_SECRET
|
|
| 35 |
supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
|
| 36 |
groq_client = Groq(api_key=GROQ_API_KEY)
|
| 37 |
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
app.include_router(whatsapp_router)
|
| 40 |
|
| 41 |
# --- RATE LIMITER CONFIG ---
|
|
@@ -57,6 +85,7 @@ from middleware.memory_guard import MemoryGuardMiddleware
|
|
| 57 |
app.add_middleware(MemoryGuardMiddleware)
|
| 58 |
|
| 59 |
|
|
|
|
| 60 |
# --- MODELS ---
|
| 61 |
class SearchQuery(BaseModel):
|
| 62 |
question: str = Field(..., min_length=3, max_length=500)
|
|
@@ -128,29 +157,28 @@ class GazetteSearchQuery(BaseModel):
|
|
| 128 |
return v.lower()
|
| 129 |
|
| 130 |
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
|
| 135 |
-
def get_embedding_model():
|
| 136 |
-
global embedding_model
|
| 137 |
-
if embedding_model is None:
|
| 138 |
-
print("⏳ Loading Nomic Embedding Model (768-dim)...")
|
| 139 |
-
# PENDING: Run 003_upgrade_embeddings_768.sql in Supabase before this goes live
|
| 140 |
-
embedding_model = SentenceTransformer(
|
| 141 |
-
'nomic-ai/nomic-embed-text-v1',
|
| 142 |
-
trust_remote_code=True
|
| 143 |
-
)
|
| 144 |
-
print("✅ Nomic Model loaded")
|
| 145 |
-
return embedding_model
|
| 146 |
|
| 147 |
-
def get_reranker_model():
|
| 148 |
-
global reranker_model
|
| 149 |
-
if reranker_model is None:
|
| 150 |
-
print("⏳ Loading Cross-Encoder Reranker Model...")
|
| 151 |
-
reranker_model = CrossEncoder('cross-encoder/ettin-reranker-68m-v1', max_length=512, device='cpu', trust_remote_code=True)
|
| 152 |
-
print("✅ Reranker Model loaded")
|
| 153 |
-
return reranker_model
|
| 154 |
|
| 155 |
def chunk_text(text: str, chunk_size: int = 500, overlap: int = 50) -> list[str]:
|
| 156 |
paragraphs = [p.strip() for p in text.split('\n\n') if p.strip()]
|
|
@@ -220,7 +248,7 @@ def rerank_gazette_chunks(query: str, chunks: list) -> list:
|
|
| 220 |
if not chunks:
|
| 221 |
return []
|
| 222 |
|
| 223 |
-
RELEVANCE_FLOOR = 5.0 # Hard quality gate
|
| 224 |
|
| 225 |
# Gazette-specific: wider pool (30) because legislative text
|
| 226 |
# has higher semantic density than scheme descriptions
|
|
@@ -554,15 +582,16 @@ async def gazette_search(request: Request, query: GazetteSearchQuery):
|
|
| 554 |
4. Cross-encoder re-rank top 30 with Ettin
|
| 555 |
5. Return page-pinned results to frontend GazetteViewer
|
| 556 |
"""
|
| 557 |
-
|
| 558 |
|
| 559 |
try:
|
| 560 |
# Step 1: Embed query
|
| 561 |
model = get_embedding_model()
|
| 562 |
-
|
| 563 |
f"search_query: {query.query}",
|
| 564 |
normalize_embeddings=True
|
| 565 |
-
)
|
|
|
|
| 566 |
|
| 567 |
# Step 2: Call INDRA oversampling RPC
|
| 568 |
rpc_params: Dict[str, Any] = {
|
|
@@ -588,15 +617,13 @@ async def gazette_search(request: Request, query: GazetteSearchQuery):
|
|
| 588 |
}
|
| 589 |
|
| 590 |
n_candidates = len(raw_results)
|
| 591 |
-
|
| 592 |
|
| 593 |
# Step 3: Poincaré projection & hyperbolic re-ranking
|
| 594 |
-
import numpy as np
|
| 595 |
-
|
| 596 |
# Extract embedding vectors from RPC results
|
| 597 |
query_vec = np.array(query_embedding, dtype=np.float32)
|
| 598 |
candidate_embeddings = np.array(
|
| 599 |
-
[r["embedding"] for r in raw_results],
|
| 600 |
dtype=np.float32
|
| 601 |
)
|
| 602 |
|
|
@@ -619,7 +646,7 @@ async def gazette_search(request: Request, query: GazetteSearchQuery):
|
|
| 619 |
entry.pop("embedding", None)
|
| 620 |
hyperbolic_ranked.append(entry)
|
| 621 |
|
| 622 |
-
|
| 623 |
f"top-30 distance={hyperbolic_ranked[-1]['hyperbolic_distance']:.4f}")
|
| 624 |
|
| 625 |
# Step 4: Cross-encoder re-rank (Ettin verifies semantic coherence)
|
|
@@ -646,10 +673,10 @@ async def gazette_search(request: Request, query: GazetteSearchQuery):
|
|
| 646 |
})
|
| 647 |
|
| 648 |
if results:
|
| 649 |
-
|
| 650 |
f"(best CE score: {results[0]['cross_encoder_score']:.4f})")
|
| 651 |
else:
|
| 652 |
-
|
| 653 |
|
| 654 |
return {
|
| 655 |
"query": query.query,
|
|
@@ -659,12 +686,135 @@ async def gazette_search(request: Request, query: GazetteSearchQuery):
|
|
| 659 |
}
|
| 660 |
|
| 661 |
except Exception as e:
|
| 662 |
-
|
| 663 |
return {
|
| 664 |
"query": query.query,
|
| 665 |
"results": [],
|
| 666 |
"total": 0,
|
| 667 |
-
"error":
|
| 668 |
"pipeline": "indra_poincare + ettin_reranker"
|
| 669 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 670 |
|
|
|
|
| 4 |
import re
|
| 5 |
from datetime import datetime
|
| 6 |
from typing import Optional, Any, List, Dict, cast, AsyncGenerator
|
| 7 |
+
import numpy as np
|
| 8 |
+
import json
|
| 9 |
+
import logging
|
| 10 |
+
from contextlib import asynccontextmanager
|
| 11 |
from fastapi import FastAPI, Request
|
| 12 |
from fastapi.responses import StreamingResponse
|
| 13 |
from pydantic import BaseModel, Field, field_validator
|
|
|
|
| 30 |
from indra_engine import IndraProjectionEngine
|
| 31 |
indra_engine = IndraProjectionEngine(batch_size=400, dimensions=768)
|
| 32 |
|
| 33 |
+
# Configure logging
|
| 34 |
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")
|
| 35 |
+
logger = logging.getLogger("govbridge.api")
|
| 36 |
+
|
| 37 |
+
# Variables for AI models
|
| 38 |
+
embedding_model = None
|
| 39 |
+
cross_encoder_model = None
|
| 40 |
+
|
| 41 |
+
def get_embedding_model():
|
| 42 |
+
return embedding_model
|
| 43 |
+
|
| 44 |
+
def get_reranker_model():
|
| 45 |
+
return cross_encoder_model
|
| 46 |
+
|
| 47 |
# --- SECURE KEYS ---
|
| 48 |
SUPABASE_URL = settings.SUPABASE_URL
|
| 49 |
SUPABASE_KEY = settings.SUPABASE_KEY
|
|
|
|
| 53 |
supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
|
| 54 |
groq_client = Groq(api_key=GROQ_API_KEY)
|
| 55 |
|
| 56 |
+
@asynccontextmanager
|
| 57 |
+
async def lifespan(app: FastAPI):
|
| 58 |
+
global embedding_model, cross_encoder_model
|
| 59 |
+
logger.info("⏳ Pre-loading Nomic Embedding Model (768-dim)...")
|
| 60 |
+
embedding_model = SentenceTransformer("nomic-ai/nomic-embed-text-v1", trust_remote_code=True)
|
| 61 |
+
logger.info("⏳ Pre-loading Cross-Encoder Reranker Model...")
|
| 62 |
+
cross_encoder_model = CrossEncoder("cross-encoder/ettin-reranker-68m-v1", max_length=512, device='cpu', trust_remote_code=True)
|
| 63 |
+
logger.info("✅ Models pre-loaded successfully.")
|
| 64 |
+
yield
|
| 65 |
+
|
| 66 |
+
app = FastAPI(lifespan=lifespan)
|
| 67 |
app.include_router(whatsapp_router)
|
| 68 |
|
| 69 |
# --- RATE LIMITER CONFIG ---
|
|
|
|
| 85 |
app.add_middleware(MemoryGuardMiddleware)
|
| 86 |
|
| 87 |
|
| 88 |
+
|
| 89 |
# --- MODELS ---
|
| 90 |
class SearchQuery(BaseModel):
|
| 91 |
question: str = Field(..., min_length=3, max_length=500)
|
|
|
|
| 157 |
return v.lower()
|
| 158 |
|
| 159 |
|
| 160 |
+
class GraphNode(BaseModel):
|
| 161 |
+
"""A node in the knowledge graph visualization."""
|
| 162 |
+
id: str
|
| 163 |
+
name: str
|
| 164 |
+
group: str # 'anchor' | 'hop1' | 'hop2'
|
| 165 |
+
|
| 166 |
+
class GraphLink(BaseModel):
|
| 167 |
+
"""A directional edge in the knowledge graph visualization."""
|
| 168 |
+
source: str
|
| 169 |
+
target: str
|
| 170 |
+
label: str
|
| 171 |
+
hop: int
|
| 172 |
+
|
| 173 |
+
class GraphResponse(BaseModel):
|
| 174 |
+
"""Force-graph-compatible response payload."""
|
| 175 |
+
anchor_id: str
|
| 176 |
+
nodes: List[GraphNode]
|
| 177 |
+
links: List[GraphLink]
|
| 178 |
+
total_nodes: int
|
| 179 |
+
total_links: int
|
| 180 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
|
| 183 |
def chunk_text(text: str, chunk_size: int = 500, overlap: int = 50) -> list[str]:
|
| 184 |
paragraphs = [p.strip() for p in text.split('\n\n') if p.strip()]
|
|
|
|
| 248 |
if not chunks:
|
| 249 |
return []
|
| 250 |
|
| 251 |
+
RELEVANCE_FLOOR = 5.0 # Hard quality gate restored for production
|
| 252 |
|
| 253 |
# Gazette-specific: wider pool (30) because legislative text
|
| 254 |
# has higher semantic density than scheme descriptions
|
|
|
|
| 582 |
4. Cross-encoder re-rank top 30 with Ettin
|
| 583 |
5. Return page-pinned results to frontend GazetteViewer
|
| 584 |
"""
|
| 585 |
+
logger.info(f"📜 Gazette search [INDRA]: '{query.query}' | Type: {query.gazette_type} | State: {query.state}")
|
| 586 |
|
| 587 |
try:
|
| 588 |
# Step 1: Embed query
|
| 589 |
model = get_embedding_model()
|
| 590 |
+
query_embedding_raw = model.encode(
|
| 591 |
f"search_query: {query.query}",
|
| 592 |
normalize_embeddings=True
|
| 593 |
+
)
|
| 594 |
+
query_embedding = np.array(query_embedding_raw).flatten().tolist()
|
| 595 |
|
| 596 |
# Step 2: Call INDRA oversampling RPC
|
| 597 |
rpc_params: Dict[str, Any] = {
|
|
|
|
| 617 |
}
|
| 618 |
|
| 619 |
n_candidates = len(raw_results)
|
| 620 |
+
logger.info(f" 📊 INDRA: {n_candidates} Euclidean candidates retrieved")
|
| 621 |
|
| 622 |
# Step 3: Poincaré projection & hyperbolic re-ranking
|
|
|
|
|
|
|
| 623 |
# Extract embedding vectors from RPC results
|
| 624 |
query_vec = np.array(query_embedding, dtype=np.float32)
|
| 625 |
candidate_embeddings = np.array(
|
| 626 |
+
[json.loads(r["embedding"]) if isinstance(r["embedding"], str) else r["embedding"] for r in raw_results],
|
| 627 |
dtype=np.float32
|
| 628 |
)
|
| 629 |
|
|
|
|
| 646 |
entry.pop("embedding", None)
|
| 647 |
hyperbolic_ranked.append(entry)
|
| 648 |
|
| 649 |
+
logger.info(f" 🔮 Poincaré: top-1 distance={hyperbolic_ranked[0]['hyperbolic_distance']:.4f}, "
|
| 650 |
f"top-30 distance={hyperbolic_ranked[-1]['hyperbolic_distance']:.4f}")
|
| 651 |
|
| 652 |
# Step 4: Cross-encoder re-rank (Ettin verifies semantic coherence)
|
|
|
|
| 673 |
})
|
| 674 |
|
| 675 |
if results:
|
| 676 |
+
logger.info(f" ✅ Returning {len(results)} gazette results "
|
| 677 |
f"(best CE score: {results[0]['cross_encoder_score']:.4f})")
|
| 678 |
else:
|
| 679 |
+
logger.info(" ⛔ No results survived quality gate")
|
| 680 |
|
| 681 |
return {
|
| 682 |
"query": query.query,
|
|
|
|
| 686 |
}
|
| 687 |
|
| 688 |
except Exception as e:
|
| 689 |
+
logger.error(f"❌ Gazette search [INDRA] error: {e}", exc_info=True)
|
| 690 |
return {
|
| 691 |
"query": query.query,
|
| 692 |
"results": [],
|
| 693 |
"total": 0,
|
| 694 |
+
"error": "An internal error occurred during the search. Please try again later.",
|
| 695 |
"pipeline": "indra_poincare + ettin_reranker"
|
| 696 |
}
|
| 697 |
+
|
| 698 |
+
# ═══════════════════════════════════════════════════════════════
|
| 699 |
+
# PROJECT INDRA Phase 3.1 — Knowledge Graph Neighborhood API
|
| 700 |
+
# Sprint 34
|
| 701 |
+
# ═══════════════════════════════════════════════════════════════
|
| 702 |
+
|
| 703 |
+
@app.get("/api/gazette/graph/{chunk_id}")
|
| 704 |
+
@limiter.limit("30/minute")
|
| 705 |
+
async def get_graph_neighborhood(request: Request, chunk_id: str, depth: int = 1, limit: int = 100):
|
| 706 |
+
"""
|
| 707 |
+
Knowledge Graph Neighborhood — PROJECT INDRA Phase 3.1 (Sprint 34).
|
| 708 |
+
|
| 709 |
+
Returns the 1-hop or 2-hop graph neighborhood of a gazette document,
|
| 710 |
+
formatted as a node-link structure for force-directed graph rendering.
|
| 711 |
+
|
| 712 |
+
Args:
|
| 713 |
+
chunk_id: UUID of the anchor gazette_chunks document.
|
| 714 |
+
depth: Traversal depth (1 or 2). Default 1.
|
| 715 |
+
limit: Max edges to return. Default 100, max 500.
|
| 716 |
+
"""
|
| 717 |
+
logger.info(f"🔗 Graph neighborhood: chunk_id={chunk_id} | depth={depth} | limit={limit}")
|
| 718 |
+
|
| 719 |
+
try:
|
| 720 |
+
# Validate UUID format
|
| 721 |
+
import uuid as uuid_mod
|
| 722 |
+
try:
|
| 723 |
+
uuid_mod.UUID(chunk_id, version=4)
|
| 724 |
+
except ValueError:
|
| 725 |
+
return {
|
| 726 |
+
"anchor_id": chunk_id,
|
| 727 |
+
"nodes": [],
|
| 728 |
+
"links": [],
|
| 729 |
+
"total_nodes": 0,
|
| 730 |
+
"total_links": 0,
|
| 731 |
+
"error": "Invalid UUID format"
|
| 732 |
+
}
|
| 733 |
+
|
| 734 |
+
# Clamp parameters
|
| 735 |
+
depth = max(1, min(2, depth))
|
| 736 |
+
limit = max(1, min(500, limit))
|
| 737 |
+
|
| 738 |
+
# Call the graph neighborhood RPC
|
| 739 |
+
result = supabase.rpc("get_graph_neighborhood", {
|
| 740 |
+
"anchor_id": chunk_id,
|
| 741 |
+
"max_depth": depth,
|
| 742 |
+
"edge_limit": limit,
|
| 743 |
+
}).execute()
|
| 744 |
+
|
| 745 |
+
raw_edges = cast(List[Dict[str, Any]], result.data or [])
|
| 746 |
+
|
| 747 |
+
if not raw_edges:
|
| 748 |
+
logger.info(f" ⛔ No graph edges found for chunk_id={chunk_id}")
|
| 749 |
+
return {
|
| 750 |
+
"anchor_id": chunk_id,
|
| 751 |
+
"nodes": [],
|
| 752 |
+
"links": [],
|
| 753 |
+
"total_nodes": 0,
|
| 754 |
+
"total_links": 0
|
| 755 |
+
}
|
| 756 |
+
|
| 757 |
+
# ── Transform edge-centric data to node-link format ──
|
| 758 |
+
nodes_map: Dict[str, GraphNode] = {}
|
| 759 |
+
links: List[GraphLink] = []
|
| 760 |
+
|
| 761 |
+
for edge in raw_edges:
|
| 762 |
+
source_id = edge.get("source_id", "")
|
| 763 |
+
target_id = edge.get("target_id", "")
|
| 764 |
+
source_title = edge.get("source_title", "Unknown Document")
|
| 765 |
+
target_title = edge.get("target_title", "Unresolved Entity")
|
| 766 |
+
edge_type = edge.get("edge_type", "cross_references")
|
| 767 |
+
hop_depth = edge.get("hop_depth", 1)
|
| 768 |
+
|
| 769 |
+
# Skip edges with null target (unresolved entities from Sprint 30)
|
| 770 |
+
if not target_id:
|
| 771 |
+
continue
|
| 772 |
+
|
| 773 |
+
# Deduplicate nodes — assign group based on relationship to anchor
|
| 774 |
+
if source_id and source_id not in nodes_map:
|
| 775 |
+
group = "anchor" if source_id == chunk_id else f"hop{hop_depth}"
|
| 776 |
+
nodes_map[source_id] = GraphNode(
|
| 777 |
+
id=source_id,
|
| 778 |
+
name=source_title[:80], # Truncate long titles for Canvas rendering
|
| 779 |
+
group=group,
|
| 780 |
+
)
|
| 781 |
+
|
| 782 |
+
if target_id and target_id not in nodes_map:
|
| 783 |
+
group = "anchor" if target_id == chunk_id else f"hop{hop_depth}"
|
| 784 |
+
nodes_map[target_id] = GraphNode(
|
| 785 |
+
id=target_id,
|
| 786 |
+
name=target_title[:80],
|
| 787 |
+
group=group,
|
| 788 |
+
)
|
| 789 |
+
|
| 790 |
+
# Create link
|
| 791 |
+
if source_id and target_id:
|
| 792 |
+
links.append(GraphLink(
|
| 793 |
+
source=source_id,
|
| 794 |
+
target=target_id,
|
| 795 |
+
label=edge_type,
|
| 796 |
+
hop=hop_depth,
|
| 797 |
+
))
|
| 798 |
+
|
| 799 |
+
nodes = list(nodes_map.values())
|
| 800 |
+
logger.info(f" ✅ Graph: {len(nodes)} nodes, {len(links)} links (depth={depth})")
|
| 801 |
+
|
| 802 |
+
return {
|
| 803 |
+
"anchor_id": chunk_id,
|
| 804 |
+
"nodes": [n.model_dump() for n in nodes],
|
| 805 |
+
"links": [l.model_dump() for l in links],
|
| 806 |
+
"total_nodes": len(nodes),
|
| 807 |
+
"total_links": len(links),
|
| 808 |
+
}
|
| 809 |
+
|
| 810 |
+
except Exception as e:
|
| 811 |
+
logger.error(f"❌ Graph neighborhood error: {e}", exc_info=True)
|
| 812 |
+
return {
|
| 813 |
+
"anchor_id": chunk_id,
|
| 814 |
+
"nodes": [],
|
| 815 |
+
"links": [],
|
| 816 |
+
"total_nodes": 0,
|
| 817 |
+
"total_links": 0,
|
| 818 |
+
"error": "An internal error occurred retrieving the graph.",
|
| 819 |
+
}
|
| 820 |
|
make_pdf.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
from playwright.async_api import async_playwright
|
| 3 |
+
|
| 4 |
+
async def make_pdf():
|
| 5 |
+
async with async_playwright() as p:
|
| 6 |
+
browser = await p.chromium.launch(headless=True, args=["--no-sandbox"])
|
| 7 |
+
page = await browser.new_page()
|
| 8 |
+
content = """
|
| 9 |
+
<h1>Gazette of India - Extraordinary</h1>
|
| 10 |
+
<h2>Part II - Section 3</h2>
|
| 11 |
+
<p><strong>Agricultural Subsidy Amendment 2026</strong></p>
|
| 12 |
+
<p>This is a real, officially ingested document to test the GovBridge system.</p>
|
| 13 |
+
<p>The Ministry of Agriculture has allocated additional funding for memory and compute infrastructure in rural farming cooperatives.</p>
|
| 14 |
+
<p>By implementing this scheme, farmers will receive unprecedented access to digital agricultural markets.</p>
|
| 15 |
+
<p>Clause 4: The state of Rajasthan shall be the primary testing ground for this new agricultural framework.</p>
|
| 16 |
+
"""
|
| 17 |
+
await page.set_content(content)
|
| 18 |
+
await page.pdf(path="test_gazette.pdf")
|
| 19 |
+
await browser.close()
|
| 20 |
+
|
| 21 |
+
if __name__ == "__main__":
|
| 22 |
+
asyncio.run(make_pdf())
|
server.log
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
INFO: Started server process [61554]
|
| 2 |
+
INFO: Waiting for application startup.
|
| 3 |
+
INFO: Application startup complete.
|
| 4 |
+
INFO: Uvicorn running on http://127.0.0.1:8888 (Press CTRL+C to quit)
|
| 5 |
+
INFO: 127.0.0.1:43166 - "POST /api/kernels HTTP/1.1" 404 Not Found
|
| 6 |
+
INFO: 127.0.0.1:52406 - "POST /api/kernels HTTP/1.1" 404 Not Found
|
| 7 |
+
INFO: 127.0.0.1:60060 - "GET / HTTP/1.1" 404 Not Found
|
| 8 |
+
Warning: You are sending unauthenticated requests to the HF Hub. Please set a HF_TOKEN to enable higher rate limits and faster downloads.
|
test_api_client.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
import os
|
| 3 |
+
import json
|
| 4 |
+
from fastapi.testclient import TestClient
|
| 5 |
+
|
| 6 |
+
# Must run from inside gov_backend directory for imports to work
|
| 7 |
+
sys.path.insert(0, os.path.abspath("."))
|
| 8 |
+
from api import app
|
| 9 |
+
|
| 10 |
+
client = TestClient(app)
|
| 11 |
+
|
| 12 |
+
print("--- Testing /health ---")
|
| 13 |
+
response = client.get("/health")
|
| 14 |
+
print(response.json())
|
| 15 |
+
|
| 16 |
+
print("\n--- Testing /api/gazette/search ---")
|
| 17 |
+
payload = {
|
| 18 |
+
"query": "solar power tax exemption",
|
| 19 |
+
"limit": 5
|
| 20 |
+
}
|
| 21 |
+
response = client.post("/api/gazette/search", json=payload)
|
| 22 |
+
data = response.json()
|
| 23 |
+
|
| 24 |
+
print(json.dumps(data, indent=2))
|
| 25 |
+
|
| 26 |
+
if data.get("results"):
|
| 27 |
+
print("\nSUCCESS: Hybrid search pipeline integrated correctly with learned weights!")
|
| 28 |
+
else:
|
| 29 |
+
print("\nNo results found, but endpoint responded correctly.")
|
test_gazette.pdf
ADDED
|
Binary file (21.6 kB). View file
|
|
|