Spaces:
Paused
Paused
Commit
Β·
5dfbe50
0
Parent(s):
Initial backend deployment - Hono proxy + ColPali embedding API
Browse files- .env.example +9 -0
- .gitignore +5 -0
- Dockerfile +70 -0
- README.md +47 -0
- embedding_api.py +166 -0
- hono-proxy/.env.backend-hf +23 -0
- hono-proxy/.env.example +22 -0
- hono-proxy/.env.hf +22 -0
- hono-proxy/.gitignore +13 -0
- hono-proxy/Dockerfile +56 -0
- hono-proxy/README-NEXTJS-COMPATIBILITY.md +127 -0
- hono-proxy/README.md +207 -0
- hono-proxy/client-example.ts +156 -0
- hono-proxy/colpali-response.json +1 -0
- hono-proxy/docker-compose.yml +29 -0
- hono-proxy/ecosystem.config.js +23 -0
- hono-proxy/package-lock.json +751 -0
- hono-proxy/package.json +26 -0
- hono-proxy/src/config/index.ts +44 -0
- hono-proxy/src/index.ts +106 -0
- hono-proxy/src/middleware/cors.ts +18 -0
- hono-proxy/src/middleware/logger.ts +13 -0
- hono-proxy/src/middleware/rateLimit.ts +54 -0
- hono-proxy/src/routes/api.ts +274 -0
- hono-proxy/src/routes/backend-api.ts +376 -0
- hono-proxy/src/routes/chat-direct.ts +46 -0
- hono-proxy/src/routes/chat.ts +109 -0
- hono-proxy/src/routes/colpali-search-vespa.ts +107 -0
- hono-proxy/src/routes/colpali-search.ts +61 -0
- hono-proxy/src/routes/full-image.ts +49 -0
- hono-proxy/src/routes/health.ts +101 -0
- hono-proxy/src/routes/query-suggestions-vespa.ts +60 -0
- hono-proxy/src/routes/query-suggestions.ts +49 -0
- hono-proxy/src/routes/search-direct.ts +230 -0
- hono-proxy/src/routes/search.ts +178 -0
- hono-proxy/src/routes/similarity-maps.ts +39 -0
- hono-proxy/src/routes/visual-rag-chat.ts +109 -0
- hono-proxy/src/services/cache.ts +68 -0
- hono-proxy/src/services/vespa-client-simple.ts +23 -0
- hono-proxy/src/services/vespa-client.ts +33 -0
- hono-proxy/src/services/vespa-https.ts +102 -0
- hono-proxy/start.sh +40 -0
- hono-proxy/tsconfig.json +18 -0
- requirements_embedding.txt +9 -0
- vespa-certs/data-plane-private-key.pem +5 -0
- vespa-certs/data-plane-public-cert.pem +9 -0
.env.example
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Vespa Configuration
|
| 2 |
+
VESPA_ENDPOINT=https://your-vespa-endpoint.vespa-cloud.com
|
| 3 |
+
VESPA_CERT_PATH=/home/user/.vespa/il-infra.colpali-server.default
|
| 4 |
+
|
| 5 |
+
# CORS Configuration
|
| 6 |
+
CORS_ORIGIN=*
|
| 7 |
+
|
| 8 |
+
# API Configuration
|
| 9 |
+
EMBEDDING_API_URL=http://localhost:8001
|
.gitignore
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.log
|
| 2 |
+
node_modules/
|
| 3 |
+
__pycache__/
|
| 4 |
+
.env.local
|
| 5 |
+
.DS_Store
|
Dockerfile
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM node:20-slim
|
| 2 |
+
|
| 3 |
+
# Install Python and system dependencies
|
| 4 |
+
RUN apt-get update && apt-get install -y \
|
| 5 |
+
python3.11 \
|
| 6 |
+
python3-pip \
|
| 7 |
+
python3.11-venv \
|
| 8 |
+
git \
|
| 9 |
+
build-essential \
|
| 10 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 11 |
+
|
| 12 |
+
# Create a non-root user (required by HF Spaces)
|
| 13 |
+
RUN useradd -m -u 1000 user
|
| 14 |
+
USER user
|
| 15 |
+
ENV HOME=/home/user \
|
| 16 |
+
PATH=/home/user/.local/bin:$PATH
|
| 17 |
+
|
| 18 |
+
# Set working directory
|
| 19 |
+
WORKDIR $HOME/app
|
| 20 |
+
|
| 21 |
+
# Copy backend files only
|
| 22 |
+
COPY --chown=user embedding_api.py $HOME/app/
|
| 23 |
+
COPY --chown=user requirements_embedding.txt $HOME/app/
|
| 24 |
+
COPY --chown=user hono-proxy $HOME/app/hono-proxy
|
| 25 |
+
|
| 26 |
+
# Create Python virtual environment
|
| 27 |
+
RUN python3.11 -m venv $HOME/venv
|
| 28 |
+
ENV PATH="$HOME/venv/bin:$PATH"
|
| 29 |
+
|
| 30 |
+
# Install Python dependencies for embedding API
|
| 31 |
+
RUN pip install --upgrade pip
|
| 32 |
+
RUN pip install -r requirements_embedding.txt
|
| 33 |
+
|
| 34 |
+
# Install pnpm and Node dependencies for Hono proxy
|
| 35 |
+
RUN npm install -g pnpm
|
| 36 |
+
WORKDIR $HOME/app/hono-proxy
|
| 37 |
+
RUN pnpm install
|
| 38 |
+
|
| 39 |
+
# Copy Vespa certificates (these need to be included in the repo)
|
| 40 |
+
RUN mkdir -p $HOME/.vespa/il-infra.colpali-server.default
|
| 41 |
+
COPY --chown=user vespa-certs/* $HOME/.vespa/il-infra.colpali-server.default/ || true
|
| 42 |
+
|
| 43 |
+
# Create startup script for backend services only
|
| 44 |
+
WORKDIR $HOME/app
|
| 45 |
+
RUN cat > start-backend.sh << 'EOF'
|
| 46 |
+
#!/bin/bash
|
| 47 |
+
|
| 48 |
+
# Start embedding API on port 8001
|
| 49 |
+
echo "Starting ColPali embedding API on port 8001..."
|
| 50 |
+
python embedding_api.py &
|
| 51 |
+
EMBED_PID=$!
|
| 52 |
+
|
| 53 |
+
# Wait for embedding API to be ready
|
| 54 |
+
sleep 10
|
| 55 |
+
|
| 56 |
+
# Start Hono proxy on HF Spaces port 7860
|
| 57 |
+
echo "Starting Hono proxy on port 7860..."
|
| 58 |
+
cd hono-proxy && PORT=7860 CORS_ORIGIN="*" EMBEDDING_API_URL="http://localhost:8001" npx tsx src/index.ts
|
| 59 |
+
|
| 60 |
+
# If Hono exits, kill embedding service
|
| 61 |
+
kill $EMBED_PID
|
| 62 |
+
EOF
|
| 63 |
+
|
| 64 |
+
RUN chmod +x start-backend.sh
|
| 65 |
+
|
| 66 |
+
# Expose HF Spaces port (Hono proxy will run on this)
|
| 67 |
+
EXPOSE 7860
|
| 68 |
+
|
| 69 |
+
# Run the startup script
|
| 70 |
+
CMD ["./start-backend.sh"]
|
README.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: ColPali Backend API
|
| 3 |
+
emoji: π
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
license: apache-2.0
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# ColPali Backend API
|
| 12 |
+
|
| 13 |
+
This Space provides the backend services for ColPali visual document retrieval:
|
| 14 |
+
- **Hono Proxy API** on port 7860
|
| 15 |
+
- **ColPali Embedding Service** on port 8001 (internal)
|
| 16 |
+
|
| 17 |
+
## API Endpoints
|
| 18 |
+
|
| 19 |
+
### Query Endpoint
|
| 20 |
+
```
|
| 21 |
+
POST /api/query
|
| 22 |
+
Content-Type: application/json
|
| 23 |
+
|
| 24 |
+
{
|
| 25 |
+
"query": "your search query",
|
| 26 |
+
"limit": 10
|
| 27 |
+
}
|
| 28 |
+
```
|
| 29 |
+
|
| 30 |
+
### Health Check
|
| 31 |
+
```
|
| 32 |
+
GET /api/health
|
| 33 |
+
```
|
| 34 |
+
|
| 35 |
+
## Usage
|
| 36 |
+
|
| 37 |
+
Configure your frontend to point to:
|
| 38 |
+
```
|
| 39 |
+
https://[your-username]-[space-name].hf.space
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
## Environment Variables
|
| 43 |
+
|
| 44 |
+
Set these in your HF Space settings:
|
| 45 |
+
- `VESPA_ENDPOINT`: Your Vespa cluster endpoint
|
| 46 |
+
- `VESPA_CERT_PATH`: Path to Vespa certificates
|
| 47 |
+
- `CORS_ORIGIN`: Allowed origins for CORS (default: *)
|
embedding_api.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
ColPali Embedding API for generating query embeddings
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import logging
|
| 8 |
+
import numpy as np
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
from typing import List, Dict
|
| 11 |
+
from fastapi import FastAPI, Query, HTTPException
|
| 12 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 13 |
+
import torch
|
| 14 |
+
from PIL import Image
|
| 15 |
+
import uvicorn
|
| 16 |
+
|
| 17 |
+
from colpali_engine.models import ColPali, ColPaliProcessor
|
| 18 |
+
from colpali_engine.utils.torch_utils import get_torch_device
|
| 19 |
+
|
| 20 |
+
# Setup logging
|
| 21 |
+
logging.basicConfig(level=logging.INFO)
|
| 22 |
+
logger = logging.getLogger(__name__)
|
| 23 |
+
|
| 24 |
+
# Initialize FastAPI
|
| 25 |
+
app = FastAPI(title="ColPali Embedding API")
|
| 26 |
+
|
| 27 |
+
# Configure CORS
|
| 28 |
+
app.add_middleware(
|
| 29 |
+
CORSMiddleware,
|
| 30 |
+
allow_origins=["http://localhost:3000", "http://localhost:3025", "http://localhost:4000"],
|
| 31 |
+
allow_credentials=True,
|
| 32 |
+
allow_methods=["*"],
|
| 33 |
+
allow_headers=["*"],
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
# Global model variables
|
| 37 |
+
model = None
|
| 38 |
+
processor = None
|
| 39 |
+
device = None
|
| 40 |
+
|
| 41 |
+
MAX_QUERY_TERMS = 64
|
| 42 |
+
|
| 43 |
+
def load_model():
|
| 44 |
+
"""Load ColPali model and processor"""
|
| 45 |
+
global model, processor, device
|
| 46 |
+
|
| 47 |
+
if model is None:
|
| 48 |
+
logger.info("Loading ColPali model...")
|
| 49 |
+
device = get_torch_device("auto")
|
| 50 |
+
logger.info(f"Using device: {device}")
|
| 51 |
+
|
| 52 |
+
try:
|
| 53 |
+
model_name = "vidore/colpali-v1.2"
|
| 54 |
+
model = ColPali.from_pretrained(
|
| 55 |
+
model_name,
|
| 56 |
+
torch_dtype=torch.bfloat16 if torch.cuda.is_available() else torch.float32,
|
| 57 |
+
device_map=device
|
| 58 |
+
).eval()
|
| 59 |
+
processor = ColPaliProcessor.from_pretrained(model_name)
|
| 60 |
+
logger.info("ColPali model loaded successfully")
|
| 61 |
+
except Exception as e:
|
| 62 |
+
logger.error(f"Error loading model: {e}")
|
| 63 |
+
# Try alternative model
|
| 64 |
+
model_name = "vidore/colpaligemma-3b-pt-448-base"
|
| 65 |
+
model = ColPali.from_pretrained(
|
| 66 |
+
model_name,
|
| 67 |
+
torch_dtype=torch.bfloat16 if torch.cuda.is_available() else torch.float32,
|
| 68 |
+
device_map=device
|
| 69 |
+
).eval()
|
| 70 |
+
processor = ColPaliProcessor.from_pretrained(model_name)
|
| 71 |
+
logger.info(f"Loaded alternative model: {model_name}")
|
| 72 |
+
|
| 73 |
+
return model, processor
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
@app.get("/health")
|
| 77 |
+
async def health():
|
| 78 |
+
"""Health check endpoint"""
|
| 79 |
+
return {"status": "healthy", "service": "colpali-embedding-api"}
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
@app.get("/embed_query")
|
| 83 |
+
async def embed_query(
|
| 84 |
+
query: str = Query(..., description="Text query to embed")
|
| 85 |
+
):
|
| 86 |
+
"""Generate ColPali embeddings for a text query"""
|
| 87 |
+
try:
|
| 88 |
+
model, processor = load_model()
|
| 89 |
+
|
| 90 |
+
# Create a dummy image for text-only queries
|
| 91 |
+
# ColPali expects image inputs, so we use a white image
|
| 92 |
+
dummy_image = Image.new('RGB', (448, 448), color='white')
|
| 93 |
+
|
| 94 |
+
# Process query with dummy image
|
| 95 |
+
inputs = processor(
|
| 96 |
+
images=[dummy_image],
|
| 97 |
+
text=[query],
|
| 98 |
+
return_tensors="pt",
|
| 99 |
+
padding=True
|
| 100 |
+
).to(device)
|
| 101 |
+
|
| 102 |
+
# Generate embeddings
|
| 103 |
+
with torch.no_grad():
|
| 104 |
+
embeddings = model(**inputs) # Direct output, not .last_hidden_state
|
| 105 |
+
|
| 106 |
+
# Process embeddings for Vespa format
|
| 107 |
+
# Extract query embeddings (text tokens)
|
| 108 |
+
query_embeddings = embeddings[0] # First item in batch
|
| 109 |
+
|
| 110 |
+
# Convert to list format expected by Vespa
|
| 111 |
+
float_query_embedding = {}
|
| 112 |
+
binary_query_embeddings = {}
|
| 113 |
+
|
| 114 |
+
for idx in range(min(query_embeddings.shape[0], MAX_QUERY_TERMS)):
|
| 115 |
+
embedding_vector = query_embeddings[idx].cpu().numpy().tolist()
|
| 116 |
+
float_query_embedding[str(idx)] = embedding_vector
|
| 117 |
+
|
| 118 |
+
# Create binary version
|
| 119 |
+
binary_vector = (
|
| 120 |
+
np.packbits(np.where(np.array(embedding_vector) > 0, 1, 0))
|
| 121 |
+
.astype(np.int8)
|
| 122 |
+
.tolist()
|
| 123 |
+
)
|
| 124 |
+
binary_query_embeddings[str(idx)] = binary_vector
|
| 125 |
+
|
| 126 |
+
return {
|
| 127 |
+
"query": query,
|
| 128 |
+
"embeddings": {
|
| 129 |
+
"float": float_query_embedding,
|
| 130 |
+
"binary": binary_query_embeddings
|
| 131 |
+
},
|
| 132 |
+
"num_tokens": len(float_query_embedding)
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
except Exception as e:
|
| 136 |
+
logger.error(f"Embedding error: {e}")
|
| 137 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
@app.get("/embed_query_simple")
|
| 141 |
+
async def embed_query_simple(
|
| 142 |
+
query: str = Query(..., description="Text query to embed")
|
| 143 |
+
):
|
| 144 |
+
"""Generate simplified embeddings for text query (for testing)"""
|
| 145 |
+
try:
|
| 146 |
+
# For testing, return mock embeddings
|
| 147 |
+
# In production, this would use the actual ColPali model
|
| 148 |
+
mock_embedding = [0.1] * 128 # 128-dimensional embedding
|
| 149 |
+
|
| 150 |
+
return {
|
| 151 |
+
"query": query,
|
| 152 |
+
"embedding": mock_embedding,
|
| 153 |
+
"model": "colpali-v1.2"
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
except Exception as e:
|
| 157 |
+
logger.error(f"Embedding error: {e}")
|
| 158 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
if __name__ == "__main__":
|
| 162 |
+
port = int(os.getenv("EMBEDDING_PORT", "7861"))
|
| 163 |
+
logger.info(f"Starting ColPali Embedding API on port {port}")
|
| 164 |
+
# Pre-load model
|
| 165 |
+
load_model()
|
| 166 |
+
uvicorn.run(app, host="0.0.0.0", port=port)
|
hono-proxy/.env.backend-hf
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Hugging Face Backend Deployment Configuration
|
| 2 |
+
# Jul 24 - Backend services only (Hono + ColPali)
|
| 3 |
+
|
| 4 |
+
# Server Configuration
|
| 5 |
+
PORT=7860
|
| 6 |
+
NODE_ENV=production
|
| 7 |
+
|
| 8 |
+
# CORS - Allow all origins for external frontend access
|
| 9 |
+
CORS_ORIGIN=*
|
| 10 |
+
|
| 11 |
+
# Vespa Configuration
|
| 12 |
+
VESPA_ENDPOINT=https://il-infra.colpali-server.default.vespa-cloud.com
|
| 13 |
+
VESPA_CERT_PATH=/home/user/.vespa/il-infra.colpali-server.default
|
| 14 |
+
|
| 15 |
+
# Internal Services
|
| 16 |
+
EMBEDDING_API_URL=http://localhost:8001
|
| 17 |
+
|
| 18 |
+
# API Configuration
|
| 19 |
+
MAX_QUERY_TERMS=64
|
| 20 |
+
QUERY_TIMEOUT_MS=30000
|
| 21 |
+
|
| 22 |
+
# Logging
|
| 23 |
+
LOG_LEVEL=info
|
hono-proxy/.env.example
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Server Configuration
|
| 2 |
+
PORT=4000
|
| 3 |
+
NODE_ENV=development
|
| 4 |
+
|
| 5 |
+
# Backend Configuration
|
| 6 |
+
BACKEND_URL=http://localhost:7860
|
| 7 |
+
|
| 8 |
+
# CORS Configuration
|
| 9 |
+
CORS_ORIGIN=http://localhost:3000
|
| 10 |
+
|
| 11 |
+
# Cache Configuration
|
| 12 |
+
ENABLE_CACHE=true
|
| 13 |
+
CACHE_TTL=300 # 5 minutes
|
| 14 |
+
|
| 15 |
+
# Rate Limiting
|
| 16 |
+
RATE_LIMIT_WINDOW=60000 # 1 minute in ms
|
| 17 |
+
RATE_LIMIT_MAX=100
|
| 18 |
+
|
| 19 |
+
# Vespa Configuration (if direct access needed)
|
| 20 |
+
# VESPA_APP_URL=https://your-app.vespa-app.cloud
|
| 21 |
+
# VESPA_CERT_PATH=/path/to/cert.pem
|
| 22 |
+
# VESPA_KEY_PATH=/path/to/key.pem
|
hono-proxy/.env.hf
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Server Configuration
|
| 2 |
+
PORT=4025
|
| 3 |
+
NODE_ENV=production
|
| 4 |
+
|
| 5 |
+
# Backend Configuration - Direct to Vespa
|
| 6 |
+
BACKEND_URL=https://f5acf536.ed2ceb09.z.vespa-app.cloud
|
| 7 |
+
|
| 8 |
+
# CORS Configuration - Allow all origins in HF Spaces
|
| 9 |
+
CORS_ORIGIN=*
|
| 10 |
+
|
| 11 |
+
# Cache Configuration
|
| 12 |
+
ENABLE_CACHE=true
|
| 13 |
+
CACHE_TTL=300
|
| 14 |
+
|
| 15 |
+
# Rate Limiting
|
| 16 |
+
RATE_LIMIT_WINDOW=60000
|
| 17 |
+
RATE_LIMIT_MAX=100
|
| 18 |
+
|
| 19 |
+
# Vespa Configuration
|
| 20 |
+
VESPA_APP_URL=https://f5acf536.ed2ceb09.z.vespa-app.cloud
|
| 21 |
+
VESPA_CERT_PATH=/home/user/.vespa/il-infra.colpali-server.default/data-plane-public-cert.pem
|
| 22 |
+
VESPA_KEY_PATH=/home/user/.vespa/il-infra.colpali-server.default/data-plane-private-key.pem
|
hono-proxy/.gitignore
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
node_modules/
|
| 2 |
+
dist/
|
| 3 |
+
.env
|
| 4 |
+
.env.local
|
| 5 |
+
.DS_Store
|
| 6 |
+
*.log
|
| 7 |
+
npm-debug.log*
|
| 8 |
+
yarn-debug.log*
|
| 9 |
+
yarn-error.log*
|
| 10 |
+
coverage/
|
| 11 |
+
.nyc_output/
|
| 12 |
+
.vscode/
|
| 13 |
+
.idea/
|
hono-proxy/Dockerfile
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Build stage
|
| 2 |
+
FROM node:20-alpine AS builder
|
| 3 |
+
|
| 4 |
+
WORKDIR /app
|
| 5 |
+
|
| 6 |
+
# Copy package files
|
| 7 |
+
COPY package*.json ./
|
| 8 |
+
COPY tsconfig.json ./
|
| 9 |
+
|
| 10 |
+
# Install dependencies
|
| 11 |
+
RUN npm ci
|
| 12 |
+
|
| 13 |
+
# Copy source code
|
| 14 |
+
COPY src ./src
|
| 15 |
+
|
| 16 |
+
# Build the application
|
| 17 |
+
RUN npm run build
|
| 18 |
+
|
| 19 |
+
# Production stage
|
| 20 |
+
FROM node:20-alpine
|
| 21 |
+
|
| 22 |
+
WORKDIR /app
|
| 23 |
+
|
| 24 |
+
# Install dumb-init for proper signal handling
|
| 25 |
+
RUN apk add --no-cache dumb-init
|
| 26 |
+
|
| 27 |
+
# Create non-root user
|
| 28 |
+
RUN addgroup -g 1001 -S nodejs && \
|
| 29 |
+
adduser -S nodejs -u 1001
|
| 30 |
+
|
| 31 |
+
# Copy package files
|
| 32 |
+
COPY package*.json ./
|
| 33 |
+
|
| 34 |
+
# Install production dependencies only
|
| 35 |
+
RUN npm ci --only=production && \
|
| 36 |
+
npm cache clean --force
|
| 37 |
+
|
| 38 |
+
# Copy built application from builder
|
| 39 |
+
COPY --from=builder /app/dist ./dist
|
| 40 |
+
|
| 41 |
+
# Change ownership
|
| 42 |
+
RUN chown -R nodejs:nodejs /app
|
| 43 |
+
|
| 44 |
+
# Switch to non-root user
|
| 45 |
+
USER nodejs
|
| 46 |
+
|
| 47 |
+
# Expose port
|
| 48 |
+
EXPOSE 4000
|
| 49 |
+
|
| 50 |
+
# Health check
|
| 51 |
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
| 52 |
+
CMD node -e "require('http').get('http://localhost:4000/health/live', (r) => r.statusCode === 200 ? process.exit(0) : process.exit(1))"
|
| 53 |
+
|
| 54 |
+
# Use dumb-init to handle signals properly
|
| 55 |
+
ENTRYPOINT ["dumb-init", "--"]
|
| 56 |
+
CMD ["node", "dist/index.js"]
|
hono-proxy/README-NEXTJS-COMPATIBILITY.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Hono Proxy - Next.js Compatibility Guide
|
| 2 |
+
|
| 3 |
+
This Hono proxy server is designed to be a **drop-in replacement** for the Next.js API routes, providing 100% compatibility with the existing frontend.
|
| 4 |
+
|
| 5 |
+
## Endpoint Mapping
|
| 6 |
+
|
| 7 |
+
The Hono proxy implements all endpoints exactly as they exist in the Next.js implementation:
|
| 8 |
+
|
| 9 |
+
| Next.js API Route | Backend Endpoint | Method | Description |
|
| 10 |
+
|-------------------|------------------|---------|-------------|
|
| 11 |
+
| `/api/colpali-search` | `/fetch_results` | GET | Search with ColPali ranking |
|
| 12 |
+
| `/api/full-image` | `/full_image` | GET | Get full resolution image |
|
| 13 |
+
| `/api/query-suggestions` | `/suggestions` | GET | Autocomplete suggestions |
|
| 14 |
+
| `/api/similarity-maps` | `/get_sim_map` | GET | Generate similarity visualization |
|
| 15 |
+
| `/api/visual-rag-chat` | `/get-message` | GET (SSE) | Stream chat responses |
|
| 16 |
+
|
| 17 |
+
## Parameter Compatibility
|
| 18 |
+
|
| 19 |
+
All query parameters are preserved exactly as in Next.js:
|
| 20 |
+
|
| 21 |
+
### Search
|
| 22 |
+
```
|
| 23 |
+
GET /api/colpali-search?query=annual+report&ranking=hybrid
|
| 24 |
+
```
|
| 25 |
+
|
| 26 |
+
### Full Image
|
| 27 |
+
```
|
| 28 |
+
GET /api/full-image?docId=abc123
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
### Suggestions
|
| 32 |
+
```
|
| 33 |
+
GET /api/query-suggestions?query=ann
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
### Similarity Maps
|
| 37 |
+
```
|
| 38 |
+
GET /api/similarity-maps?queryId=123&idx=0&token=report&tokenIdx=2
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
### Visual RAG Chat (SSE)
|
| 42 |
+
```
|
| 43 |
+
GET /api/visual-rag-chat?queryId=123&query=What+is+revenue&docIds=abc,def,ghi
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
## Frontend Integration
|
| 47 |
+
|
| 48 |
+
To use the Hono proxy with your Next.js frontend:
|
| 49 |
+
|
| 50 |
+
1. Update your environment variable:
|
| 51 |
+
```env
|
| 52 |
+
# .env.local
|
| 53 |
+
NEXT_PUBLIC_API_URL=http://localhost:4000
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
2. Update your API calls (if using relative paths):
|
| 57 |
+
```typescript
|
| 58 |
+
// If currently using relative paths like:
|
| 59 |
+
const response = await fetch('/api/colpali-search?query=...');
|
| 60 |
+
|
| 61 |
+
// Change to:
|
| 62 |
+
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/colpali-search?query=...`);
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
3. Or use a base URL configuration:
|
| 66 |
+
```typescript
|
| 67 |
+
// utils/api.ts
|
| 68 |
+
export const API_BASE = process.env.NEXT_PUBLIC_API_URL || '';
|
| 69 |
+
|
| 70 |
+
// In components:
|
| 71 |
+
const response = await fetch(`${API_BASE}/api/colpali-search?query=...`);
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
## Response Format
|
| 75 |
+
|
| 76 |
+
All responses are identical to what Next.js returns:
|
| 77 |
+
|
| 78 |
+
- Search results include the same Vespa response structure
|
| 79 |
+
- Full images return `{ base64_image: "..." }`
|
| 80 |
+
- Suggestions return `{ suggestions: [...] }`
|
| 81 |
+
- Similarity maps return HTML content
|
| 82 |
+
- SSE chat streams the same event format
|
| 83 |
+
|
| 84 |
+
## Additional Features
|
| 85 |
+
|
| 86 |
+
While maintaining 100% compatibility, the Hono proxy adds:
|
| 87 |
+
|
| 88 |
+
- **Caching**: Search results and images are cached
|
| 89 |
+
- **Rate Limiting**: Prevents backend overload
|
| 90 |
+
- **Health Checks**: Monitor backend availability
|
| 91 |
+
- **Request IDs**: Track requests across systems
|
| 92 |
+
- **Performance**: Faster response times with caching
|
| 93 |
+
|
| 94 |
+
## Migration Path
|
| 95 |
+
|
| 96 |
+
1. **No Frontend Changes Required**: The Hono proxy mimics Next.js API routes exactly
|
| 97 |
+
2. **Gradual Migration**: Can run both Next.js and Hono simultaneously on different ports
|
| 98 |
+
3. **Environment-based**: Use environment variables to switch between implementations
|
| 99 |
+
|
| 100 |
+
## Testing Compatibility
|
| 101 |
+
|
| 102 |
+
Test script to verify all endpoints work:
|
| 103 |
+
|
| 104 |
+
```bash
|
| 105 |
+
# Search
|
| 106 |
+
curl "http://localhost:4000/api/colpali-search?query=annual+report&ranking=hybrid"
|
| 107 |
+
|
| 108 |
+
# Full Image
|
| 109 |
+
curl "http://localhost:4000/api/full-image?docId=abc123"
|
| 110 |
+
|
| 111 |
+
# Suggestions
|
| 112 |
+
curl "http://localhost:4000/api/query-suggestions?query=ann"
|
| 113 |
+
|
| 114 |
+
# Similarity Map
|
| 115 |
+
curl "http://localhost:4000/api/similarity-maps?queryId=123&idx=0&token=report&tokenIdx=2"
|
| 116 |
+
|
| 117 |
+
# Visual RAG Chat (SSE)
|
| 118 |
+
curl -N "http://localhost:4000/api/visual-rag-chat?queryId=123&query=What+is+revenue&docIds=abc,def"
|
| 119 |
+
```
|
| 120 |
+
|
| 121 |
+
## Benefits Over Next.js API Routes
|
| 122 |
+
|
| 123 |
+
1. **Independent Scaling**: Scale API separately from frontend
|
| 124 |
+
2. **Better Performance**: Dedicated API server with caching
|
| 125 |
+
3. **Deployment Flexibility**: Deploy anywhere (Docker, K8s, serverless)
|
| 126 |
+
4. **Monitoring**: Built-in health checks and metrics
|
| 127 |
+
5. **Security**: Rate limiting and request validation
|
hono-proxy/README.md
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ColPali Hono Proxy Server
|
| 2 |
+
|
| 3 |
+
A high-performance proxy server built with Hono that sits between your Next.js frontend and the ColPali/Vespa backend. This proxy handles caching, rate limiting, CORS, and provides a clean API interface.
|
| 4 |
+
|
| 5 |
+
## Features
|
| 6 |
+
|
| 7 |
+
- **Image Retrieval**: Serves base64 images from Vespa as actual image files with proper caching
|
| 8 |
+
- **Search Proxy**: Forwards search requests with result caching
|
| 9 |
+
- **Chat SSE Proxy**: Handles Server-Sent Events for streaming chat responses
|
| 10 |
+
- **Rate Limiting**: Protects backend from overload
|
| 11 |
+
- **Caching**: In-memory cache for search results and images
|
| 12 |
+
- **Health Checks**: Kubernetes-ready health endpoints
|
| 13 |
+
- **CORS Handling**: Configurable CORS for frontend integration
|
| 14 |
+
- **Request Logging**: Detailed request/response logging with request IDs
|
| 15 |
+
|
| 16 |
+
## Architecture
|
| 17 |
+
|
| 18 |
+
```
|
| 19 |
+
Next.js App (3000) β Hono Proxy (4000) β ColPali Backend (7860)
|
| 20 |
+
β Vespa Cloud
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
## API Endpoints
|
| 24 |
+
|
| 25 |
+
### Search
|
| 26 |
+
- `POST /api/search` - Search documents
|
| 27 |
+
```json
|
| 28 |
+
{
|
| 29 |
+
"query": "annual report 2023",
|
| 30 |
+
"limit": 10,
|
| 31 |
+
"ranking": "hybrid"
|
| 32 |
+
}
|
| 33 |
+
```
|
| 34 |
+
|
| 35 |
+
### Image Retrieval
|
| 36 |
+
- `GET /api/search/image/:docId/thumbnail` - Get thumbnail image
|
| 37 |
+
- `GET /api/search/image/:docId/full` - Get full-size image
|
| 38 |
+
|
| 39 |
+
### Chat
|
| 40 |
+
- `POST /api/chat` - Stream chat responses (SSE)
|
| 41 |
+
```json
|
| 42 |
+
{
|
| 43 |
+
"messages": [{"role": "user", "content": "Tell me about..."}],
|
| 44 |
+
"context": []
|
| 45 |
+
}
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
### Similarity Map
|
| 49 |
+
- `POST /api/search/similarity-map` - Generate similarity visualization
|
| 50 |
+
|
| 51 |
+
### Health
|
| 52 |
+
- `GET /health` - Detailed health status
|
| 53 |
+
- `GET /health/live` - Liveness probe
|
| 54 |
+
- `GET /health/ready` - Readiness probe
|
| 55 |
+
|
| 56 |
+
## Setup
|
| 57 |
+
|
| 58 |
+
### Development
|
| 59 |
+
|
| 60 |
+
1. Install dependencies:
|
| 61 |
+
```bash
|
| 62 |
+
npm install
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
2. Copy environment variables:
|
| 66 |
+
```bash
|
| 67 |
+
cp .env.example .env
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
3. Update `.env` with your configuration
|
| 71 |
+
|
| 72 |
+
4. Run in development mode:
|
| 73 |
+
```bash
|
| 74 |
+
npm run dev
|
| 75 |
+
```
|
| 76 |
+
|
| 77 |
+
### Production
|
| 78 |
+
|
| 79 |
+
1. Build:
|
| 80 |
+
```bash
|
| 81 |
+
npm run build
|
| 82 |
+
```
|
| 83 |
+
|
| 84 |
+
2. Run:
|
| 85 |
+
```bash
|
| 86 |
+
npm start
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
### Docker
|
| 90 |
+
|
| 91 |
+
Build and run with Docker:
|
| 92 |
+
```bash
|
| 93 |
+
docker build -t colpali-hono-proxy .
|
| 94 |
+
docker run -p 4000:4000 --env-file .env colpali-hono-proxy
|
| 95 |
+
```
|
| 96 |
+
|
| 97 |
+
Or use docker-compose:
|
| 98 |
+
```bash
|
| 99 |
+
docker-compose up
|
| 100 |
+
```
|
| 101 |
+
|
| 102 |
+
## Environment Variables
|
| 103 |
+
|
| 104 |
+
| Variable | Description | Default |
|
| 105 |
+
|----------|-------------|---------|
|
| 106 |
+
| `PORT` | Server port | 4000 |
|
| 107 |
+
| `BACKEND_URL` | ColPali backend URL | http://localhost:7860 |
|
| 108 |
+
| `CORS_ORIGIN` | Allowed CORS origin | http://localhost:3000 |
|
| 109 |
+
| `ENABLE_CACHE` | Enable caching | true |
|
| 110 |
+
| `CACHE_TTL` | Cache TTL in seconds | 300 |
|
| 111 |
+
| `RATE_LIMIT_WINDOW` | Rate limit window (ms) | 60000 |
|
| 112 |
+
| `RATE_LIMIT_MAX` | Max requests per window | 100 |
|
| 113 |
+
|
| 114 |
+
## Integration with Next.js
|
| 115 |
+
|
| 116 |
+
Update your Next.js app to use the proxy:
|
| 117 |
+
|
| 118 |
+
```typescript
|
| 119 |
+
// .env.local
|
| 120 |
+
NEXT_PUBLIC_API_URL=http://localhost:4000/api
|
| 121 |
+
|
| 122 |
+
// API calls
|
| 123 |
+
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/search`, {
|
| 124 |
+
method: 'POST',
|
| 125 |
+
headers: { 'Content-Type': 'application/json' },
|
| 126 |
+
body: JSON.stringify({ query, limit })
|
| 127 |
+
});
|
| 128 |
+
```
|
| 129 |
+
|
| 130 |
+
## Caching Strategy
|
| 131 |
+
|
| 132 |
+
- **Search Results**: Cached for 5 minutes (configurable)
|
| 133 |
+
- **Images**: Cached for 24 hours
|
| 134 |
+
- **Cache Keys**: Based on query parameters
|
| 135 |
+
- **Cache Headers**: `X-Cache: HIT/MISS`
|
| 136 |
+
|
| 137 |
+
## Rate Limiting
|
| 138 |
+
|
| 139 |
+
- Default: 100 requests per minute per IP
|
| 140 |
+
- Headers included:
|
| 141 |
+
- `X-RateLimit-Limit`
|
| 142 |
+
- `X-RateLimit-Remaining`
|
| 143 |
+
- `X-RateLimit-Reset`
|
| 144 |
+
|
| 145 |
+
## Monitoring
|
| 146 |
+
|
| 147 |
+
The proxy includes:
|
| 148 |
+
- Request logging with correlation IDs
|
| 149 |
+
- Performance timing
|
| 150 |
+
- Error tracking
|
| 151 |
+
- Health endpoints for monitoring
|
| 152 |
+
|
| 153 |
+
## Deployment Options
|
| 154 |
+
|
| 155 |
+
### Railway/Fly.io
|
| 156 |
+
```toml
|
| 157 |
+
# fly.toml
|
| 158 |
+
app = "colpali-proxy"
|
| 159 |
+
primary_region = "ord"
|
| 160 |
+
|
| 161 |
+
[http_service]
|
| 162 |
+
internal_port = 4000
|
| 163 |
+
force_https = true
|
| 164 |
+
auto_stop_machines = true
|
| 165 |
+
auto_start_machines = true
|
| 166 |
+
```
|
| 167 |
+
|
| 168 |
+
### Kubernetes
|
| 169 |
+
```yaml
|
| 170 |
+
apiVersion: apps/v1
|
| 171 |
+
kind: Deployment
|
| 172 |
+
metadata:
|
| 173 |
+
name: colpali-proxy
|
| 174 |
+
spec:
|
| 175 |
+
replicas: 3
|
| 176 |
+
template:
|
| 177 |
+
spec:
|
| 178 |
+
containers:
|
| 179 |
+
- name: proxy
|
| 180 |
+
image: colpali-proxy:latest
|
| 181 |
+
ports:
|
| 182 |
+
- containerPort: 4000
|
| 183 |
+
livenessProbe:
|
| 184 |
+
httpGet:
|
| 185 |
+
path: /health/live
|
| 186 |
+
port: 4000
|
| 187 |
+
readinessProbe:
|
| 188 |
+
httpGet:
|
| 189 |
+
path: /health/ready
|
| 190 |
+
port: 4000
|
| 191 |
+
```
|
| 192 |
+
|
| 193 |
+
## Performance
|
| 194 |
+
|
| 195 |
+
- Built with Hono for maximum performance
|
| 196 |
+
- Efficient streaming for SSE
|
| 197 |
+
- Connection pooling for backend requests
|
| 198 |
+
- In-memory caching reduces backend load
|
| 199 |
+
- Brotli/gzip compression enabled
|
| 200 |
+
|
| 201 |
+
## Security
|
| 202 |
+
|
| 203 |
+
- Rate limiting prevents abuse
|
| 204 |
+
- Secure headers enabled
|
| 205 |
+
- CORS properly configured
|
| 206 |
+
- Request ID tracking
|
| 207 |
+
- No sensitive data logging
|
hono-proxy/client-example.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Example client for integrating with the Hono proxy from Next.js
|
| 3 |
+
* Place this in your Next.js app at: lib/api-client.ts
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000/api';
|
| 7 |
+
|
| 8 |
+
export interface SearchResult {
|
| 9 |
+
root: {
|
| 10 |
+
children: Array<{
|
| 11 |
+
id: string;
|
| 12 |
+
relevance: number;
|
| 13 |
+
fields: {
|
| 14 |
+
id: string;
|
| 15 |
+
title: string;
|
| 16 |
+
page_number: number;
|
| 17 |
+
text: string;
|
| 18 |
+
image: string; // base64
|
| 19 |
+
image_url: string; // Added by proxy
|
| 20 |
+
full_image_url: string; // Added by proxy
|
| 21 |
+
};
|
| 22 |
+
}>;
|
| 23 |
+
};
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
export interface ChatMessage {
|
| 27 |
+
role: 'user' | 'assistant' | 'system';
|
| 28 |
+
content: string;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
class ColPaliClient {
|
| 32 |
+
private async fetchWithTimeout(url: string, options: RequestInit, timeout = 30000) {
|
| 33 |
+
const controller = new AbortController();
|
| 34 |
+
const id = setTimeout(() => controller.abort(), timeout);
|
| 35 |
+
|
| 36 |
+
try {
|
| 37 |
+
const response = await fetch(url, {
|
| 38 |
+
...options,
|
| 39 |
+
signal: controller.signal,
|
| 40 |
+
});
|
| 41 |
+
return response;
|
| 42 |
+
} finally {
|
| 43 |
+
clearTimeout(id);
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
async search(query: string, limit = 10): Promise<SearchResult> {
|
| 48 |
+
const response = await this.fetchWithTimeout(`${API_URL}/search`, {
|
| 49 |
+
method: 'POST',
|
| 50 |
+
headers: { 'Content-Type': 'application/json' },
|
| 51 |
+
body: JSON.stringify({ query, limit }),
|
| 52 |
+
});
|
| 53 |
+
|
| 54 |
+
if (!response.ok) {
|
| 55 |
+
throw new Error(`Search failed: ${response.statusText}`);
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
return response.json();
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
async* chat(messages: ChatMessage[], context: string[] = []) {
|
| 62 |
+
const response = await fetch(`${API_URL}/chat`, {
|
| 63 |
+
method: 'POST',
|
| 64 |
+
headers: { 'Content-Type': 'application/json' },
|
| 65 |
+
body: JSON.stringify({ messages, context }),
|
| 66 |
+
});
|
| 67 |
+
|
| 68 |
+
if (!response.ok) {
|
| 69 |
+
throw new Error(`Chat failed: ${response.statusText}`);
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
const reader = response.body?.getReader();
|
| 73 |
+
if (!reader) throw new Error('No response body');
|
| 74 |
+
|
| 75 |
+
const decoder = new TextDecoder();
|
| 76 |
+
let buffer = '';
|
| 77 |
+
|
| 78 |
+
while (true) {
|
| 79 |
+
const { done, value } = await reader.read();
|
| 80 |
+
if (done) break;
|
| 81 |
+
|
| 82 |
+
buffer += decoder.decode(value, { stream: true });
|
| 83 |
+
const lines = buffer.split('\\n');
|
| 84 |
+
buffer = lines.pop() || '';
|
| 85 |
+
|
| 86 |
+
for (const line of lines) {
|
| 87 |
+
if (line.startsWith('data: ')) {
|
| 88 |
+
const data = line.slice(6);
|
| 89 |
+
if (data === '[DONE]') return;
|
| 90 |
+
|
| 91 |
+
try {
|
| 92 |
+
const parsed = JSON.parse(data);
|
| 93 |
+
yield parsed;
|
| 94 |
+
} catch (e) {
|
| 95 |
+
console.error('Failed to parse SSE data:', e);
|
| 96 |
+
}
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
}
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
async getSimilarityMap(docId: string, query: string) {
|
| 103 |
+
const response = await this.fetchWithTimeout(`${API_URL}/search/similarity-map`, {
|
| 104 |
+
method: 'POST',
|
| 105 |
+
headers: { 'Content-Type': 'application/json' },
|
| 106 |
+
body: JSON.stringify({ docId, query }),
|
| 107 |
+
});
|
| 108 |
+
|
| 109 |
+
if (!response.ok) {
|
| 110 |
+
throw new Error(`Similarity map failed: ${response.statusText}`);
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
return response.json();
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
getImageUrl(docId: string, type: 'thumbnail' | 'full' = 'thumbnail'): string {
|
| 117 |
+
return `${API_URL}/search/image/${docId}/${type}`;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
async checkHealth() {
|
| 121 |
+
const response = await this.fetchWithTimeout(`${API_URL.replace('/api', '')}/health`, {
|
| 122 |
+
method: 'GET',
|
| 123 |
+
}, 5000);
|
| 124 |
+
|
| 125 |
+
return response.json();
|
| 126 |
+
}
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
// Export singleton instance
|
| 130 |
+
export const colpaliClient = new ColPaliClient();
|
| 131 |
+
|
| 132 |
+
// Usage examples:
|
| 133 |
+
/*
|
| 134 |
+
// In your Next.js component or API route:
|
| 135 |
+
|
| 136 |
+
// Search
|
| 137 |
+
const results = await colpaliClient.search('annual report 2023', 20);
|
| 138 |
+
|
| 139 |
+
// Display images directly from proxy URLs
|
| 140 |
+
results.root.children.forEach(hit => {
|
| 141 |
+
const imageUrl = hit.fields.image_url; // Proxy URL for thumbnail
|
| 142 |
+
const fullImageUrl = hit.fields.full_image_url; // Proxy URL for full image
|
| 143 |
+
});
|
| 144 |
+
|
| 145 |
+
// Chat with streaming
|
| 146 |
+
const messages = [{ role: 'user', content: 'What is the revenue?' }];
|
| 147 |
+
for await (const chunk of colpaliClient.chat(messages)) {
|
| 148 |
+
console.log(chunk);
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
// Get image URL for direct use in <img> tags
|
| 152 |
+
const imageUrl = colpaliClient.getImageUrl('doc123', 'thumbnail');
|
| 153 |
+
|
| 154 |
+
// Check system health
|
| 155 |
+
const health = await colpaliClient.checkHealth();
|
| 156 |
+
*/
|
hono-proxy/colpali-response.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"error":"Search failed","message":"response.json is not a function"}
|
hono-proxy/docker-compose.yml
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: '3.8'
|
| 2 |
+
|
| 3 |
+
services:
|
| 4 |
+
hono-proxy:
|
| 5 |
+
build: .
|
| 6 |
+
ports:
|
| 7 |
+
- "4000:4000"
|
| 8 |
+
environment:
|
| 9 |
+
- NODE_ENV=production
|
| 10 |
+
- PORT=4000
|
| 11 |
+
- BACKEND_URL=http://backend:7860 # Adjust based on your backend service name
|
| 12 |
+
- CORS_ORIGIN=http://localhost:3000
|
| 13 |
+
- ENABLE_CACHE=true
|
| 14 |
+
- CACHE_TTL=300
|
| 15 |
+
- RATE_LIMIT_WINDOW=60000
|
| 16 |
+
- RATE_LIMIT_MAX=100
|
| 17 |
+
healthcheck:
|
| 18 |
+
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:4000/health/live"]
|
| 19 |
+
interval: 30s
|
| 20 |
+
timeout: 3s
|
| 21 |
+
retries: 3
|
| 22 |
+
start_period: 10s
|
| 23 |
+
restart: unless-stopped
|
| 24 |
+
networks:
|
| 25 |
+
- colpali-network
|
| 26 |
+
|
| 27 |
+
networks:
|
| 28 |
+
colpali-network:
|
| 29 |
+
driver: bridge
|
hono-proxy/ecosystem.config.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
module.exports = {
|
| 2 |
+
apps: [{
|
| 3 |
+
name: 'colpali-hono-proxy',
|
| 4 |
+
script: './dist/index.js',
|
| 5 |
+
instances: 'max',
|
| 6 |
+
exec_mode: 'cluster',
|
| 7 |
+
env: {
|
| 8 |
+
NODE_ENV: 'production',
|
| 9 |
+
PORT: 4000
|
| 10 |
+
},
|
| 11 |
+
error_file: './logs/error.log',
|
| 12 |
+
out_file: './logs/out.log',
|
| 13 |
+
log_file: './logs/combined.log',
|
| 14 |
+
time: true,
|
| 15 |
+
max_memory_restart: '1G',
|
| 16 |
+
autorestart: true,
|
| 17 |
+
watch: false,
|
| 18 |
+
max_restarts: 10,
|
| 19 |
+
min_uptime: '10s',
|
| 20 |
+
listen_timeout: 3000,
|
| 21 |
+
kill_timeout: 5000,
|
| 22 |
+
}]
|
| 23 |
+
};
|
hono-proxy/package-lock.json
ADDED
|
@@ -0,0 +1,751 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "colpali-hono-proxy",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"lockfileVersion": 3,
|
| 5 |
+
"requires": true,
|
| 6 |
+
"packages": {
|
| 7 |
+
"": {
|
| 8 |
+
"name": "colpali-hono-proxy",
|
| 9 |
+
"version": "1.0.0",
|
| 10 |
+
"dependencies": {
|
| 11 |
+
"@hono/node-server": "^1.8.0",
|
| 12 |
+
"@types/uuid": "^10.0.0",
|
| 13 |
+
"dotenv": "^16.4.1",
|
| 14 |
+
"hono": "^4.0.0",
|
| 15 |
+
"node-fetch": "^3.3.2",
|
| 16 |
+
"uuid": "^9.0.1",
|
| 17 |
+
"zod": "^3.22.4"
|
| 18 |
+
},
|
| 19 |
+
"devDependencies": {
|
| 20 |
+
"@types/node": "^20.11.5",
|
| 21 |
+
"tsx": "^4.7.0",
|
| 22 |
+
"typescript": "^5.3.3"
|
| 23 |
+
}
|
| 24 |
+
},
|
| 25 |
+
"node_modules/@esbuild/aix-ppc64": {
|
| 26 |
+
"version": "0.25.8",
|
| 27 |
+
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz",
|
| 28 |
+
"integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==",
|
| 29 |
+
"cpu": [
|
| 30 |
+
"ppc64"
|
| 31 |
+
],
|
| 32 |
+
"dev": true,
|
| 33 |
+
"license": "MIT",
|
| 34 |
+
"optional": true,
|
| 35 |
+
"os": [
|
| 36 |
+
"aix"
|
| 37 |
+
],
|
| 38 |
+
"engines": {
|
| 39 |
+
"node": ">=18"
|
| 40 |
+
}
|
| 41 |
+
},
|
| 42 |
+
"node_modules/@esbuild/android-arm": {
|
| 43 |
+
"version": "0.25.8",
|
| 44 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz",
|
| 45 |
+
"integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==",
|
| 46 |
+
"cpu": [
|
| 47 |
+
"arm"
|
| 48 |
+
],
|
| 49 |
+
"dev": true,
|
| 50 |
+
"license": "MIT",
|
| 51 |
+
"optional": true,
|
| 52 |
+
"os": [
|
| 53 |
+
"android"
|
| 54 |
+
],
|
| 55 |
+
"engines": {
|
| 56 |
+
"node": ">=18"
|
| 57 |
+
}
|
| 58 |
+
},
|
| 59 |
+
"node_modules/@esbuild/android-arm64": {
|
| 60 |
+
"version": "0.25.8",
|
| 61 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz",
|
| 62 |
+
"integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==",
|
| 63 |
+
"cpu": [
|
| 64 |
+
"arm64"
|
| 65 |
+
],
|
| 66 |
+
"dev": true,
|
| 67 |
+
"license": "MIT",
|
| 68 |
+
"optional": true,
|
| 69 |
+
"os": [
|
| 70 |
+
"android"
|
| 71 |
+
],
|
| 72 |
+
"engines": {
|
| 73 |
+
"node": ">=18"
|
| 74 |
+
}
|
| 75 |
+
},
|
| 76 |
+
"node_modules/@esbuild/android-x64": {
|
| 77 |
+
"version": "0.25.8",
|
| 78 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz",
|
| 79 |
+
"integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==",
|
| 80 |
+
"cpu": [
|
| 81 |
+
"x64"
|
| 82 |
+
],
|
| 83 |
+
"dev": true,
|
| 84 |
+
"license": "MIT",
|
| 85 |
+
"optional": true,
|
| 86 |
+
"os": [
|
| 87 |
+
"android"
|
| 88 |
+
],
|
| 89 |
+
"engines": {
|
| 90 |
+
"node": ">=18"
|
| 91 |
+
}
|
| 92 |
+
},
|
| 93 |
+
"node_modules/@esbuild/darwin-arm64": {
|
| 94 |
+
"version": "0.25.8",
|
| 95 |
+
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz",
|
| 96 |
+
"integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==",
|
| 97 |
+
"cpu": [
|
| 98 |
+
"arm64"
|
| 99 |
+
],
|
| 100 |
+
"dev": true,
|
| 101 |
+
"license": "MIT",
|
| 102 |
+
"optional": true,
|
| 103 |
+
"os": [
|
| 104 |
+
"darwin"
|
| 105 |
+
],
|
| 106 |
+
"engines": {
|
| 107 |
+
"node": ">=18"
|
| 108 |
+
}
|
| 109 |
+
},
|
| 110 |
+
"node_modules/@esbuild/darwin-x64": {
|
| 111 |
+
"version": "0.25.8",
|
| 112 |
+
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz",
|
| 113 |
+
"integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==",
|
| 114 |
+
"cpu": [
|
| 115 |
+
"x64"
|
| 116 |
+
],
|
| 117 |
+
"dev": true,
|
| 118 |
+
"license": "MIT",
|
| 119 |
+
"optional": true,
|
| 120 |
+
"os": [
|
| 121 |
+
"darwin"
|
| 122 |
+
],
|
| 123 |
+
"engines": {
|
| 124 |
+
"node": ">=18"
|
| 125 |
+
}
|
| 126 |
+
},
|
| 127 |
+
"node_modules/@esbuild/freebsd-arm64": {
|
| 128 |
+
"version": "0.25.8",
|
| 129 |
+
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz",
|
| 130 |
+
"integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==",
|
| 131 |
+
"cpu": [
|
| 132 |
+
"arm64"
|
| 133 |
+
],
|
| 134 |
+
"dev": true,
|
| 135 |
+
"license": "MIT",
|
| 136 |
+
"optional": true,
|
| 137 |
+
"os": [
|
| 138 |
+
"freebsd"
|
| 139 |
+
],
|
| 140 |
+
"engines": {
|
| 141 |
+
"node": ">=18"
|
| 142 |
+
}
|
| 143 |
+
},
|
| 144 |
+
"node_modules/@esbuild/freebsd-x64": {
|
| 145 |
+
"version": "0.25.8",
|
| 146 |
+
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz",
|
| 147 |
+
"integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==",
|
| 148 |
+
"cpu": [
|
| 149 |
+
"x64"
|
| 150 |
+
],
|
| 151 |
+
"dev": true,
|
| 152 |
+
"license": "MIT",
|
| 153 |
+
"optional": true,
|
| 154 |
+
"os": [
|
| 155 |
+
"freebsd"
|
| 156 |
+
],
|
| 157 |
+
"engines": {
|
| 158 |
+
"node": ">=18"
|
| 159 |
+
}
|
| 160 |
+
},
|
| 161 |
+
"node_modules/@esbuild/linux-arm": {
|
| 162 |
+
"version": "0.25.8",
|
| 163 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz",
|
| 164 |
+
"integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==",
|
| 165 |
+
"cpu": [
|
| 166 |
+
"arm"
|
| 167 |
+
],
|
| 168 |
+
"dev": true,
|
| 169 |
+
"license": "MIT",
|
| 170 |
+
"optional": true,
|
| 171 |
+
"os": [
|
| 172 |
+
"linux"
|
| 173 |
+
],
|
| 174 |
+
"engines": {
|
| 175 |
+
"node": ">=18"
|
| 176 |
+
}
|
| 177 |
+
},
|
| 178 |
+
"node_modules/@esbuild/linux-arm64": {
|
| 179 |
+
"version": "0.25.8",
|
| 180 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz",
|
| 181 |
+
"integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==",
|
| 182 |
+
"cpu": [
|
| 183 |
+
"arm64"
|
| 184 |
+
],
|
| 185 |
+
"dev": true,
|
| 186 |
+
"license": "MIT",
|
| 187 |
+
"optional": true,
|
| 188 |
+
"os": [
|
| 189 |
+
"linux"
|
| 190 |
+
],
|
| 191 |
+
"engines": {
|
| 192 |
+
"node": ">=18"
|
| 193 |
+
}
|
| 194 |
+
},
|
| 195 |
+
"node_modules/@esbuild/linux-ia32": {
|
| 196 |
+
"version": "0.25.8",
|
| 197 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz",
|
| 198 |
+
"integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==",
|
| 199 |
+
"cpu": [
|
| 200 |
+
"ia32"
|
| 201 |
+
],
|
| 202 |
+
"dev": true,
|
| 203 |
+
"license": "MIT",
|
| 204 |
+
"optional": true,
|
| 205 |
+
"os": [
|
| 206 |
+
"linux"
|
| 207 |
+
],
|
| 208 |
+
"engines": {
|
| 209 |
+
"node": ">=18"
|
| 210 |
+
}
|
| 211 |
+
},
|
| 212 |
+
"node_modules/@esbuild/linux-loong64": {
|
| 213 |
+
"version": "0.25.8",
|
| 214 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz",
|
| 215 |
+
"integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==",
|
| 216 |
+
"cpu": [
|
| 217 |
+
"loong64"
|
| 218 |
+
],
|
| 219 |
+
"dev": true,
|
| 220 |
+
"license": "MIT",
|
| 221 |
+
"optional": true,
|
| 222 |
+
"os": [
|
| 223 |
+
"linux"
|
| 224 |
+
],
|
| 225 |
+
"engines": {
|
| 226 |
+
"node": ">=18"
|
| 227 |
+
}
|
| 228 |
+
},
|
| 229 |
+
"node_modules/@esbuild/linux-mips64el": {
|
| 230 |
+
"version": "0.25.8",
|
| 231 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz",
|
| 232 |
+
"integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==",
|
| 233 |
+
"cpu": [
|
| 234 |
+
"mips64el"
|
| 235 |
+
],
|
| 236 |
+
"dev": true,
|
| 237 |
+
"license": "MIT",
|
| 238 |
+
"optional": true,
|
| 239 |
+
"os": [
|
| 240 |
+
"linux"
|
| 241 |
+
],
|
| 242 |
+
"engines": {
|
| 243 |
+
"node": ">=18"
|
| 244 |
+
}
|
| 245 |
+
},
|
| 246 |
+
"node_modules/@esbuild/linux-ppc64": {
|
| 247 |
+
"version": "0.25.8",
|
| 248 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz",
|
| 249 |
+
"integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==",
|
| 250 |
+
"cpu": [
|
| 251 |
+
"ppc64"
|
| 252 |
+
],
|
| 253 |
+
"dev": true,
|
| 254 |
+
"license": "MIT",
|
| 255 |
+
"optional": true,
|
| 256 |
+
"os": [
|
| 257 |
+
"linux"
|
| 258 |
+
],
|
| 259 |
+
"engines": {
|
| 260 |
+
"node": ">=18"
|
| 261 |
+
}
|
| 262 |
+
},
|
| 263 |
+
"node_modules/@esbuild/linux-riscv64": {
|
| 264 |
+
"version": "0.25.8",
|
| 265 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz",
|
| 266 |
+
"integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==",
|
| 267 |
+
"cpu": [
|
| 268 |
+
"riscv64"
|
| 269 |
+
],
|
| 270 |
+
"dev": true,
|
| 271 |
+
"license": "MIT",
|
| 272 |
+
"optional": true,
|
| 273 |
+
"os": [
|
| 274 |
+
"linux"
|
| 275 |
+
],
|
| 276 |
+
"engines": {
|
| 277 |
+
"node": ">=18"
|
| 278 |
+
}
|
| 279 |
+
},
|
| 280 |
+
"node_modules/@esbuild/linux-s390x": {
|
| 281 |
+
"version": "0.25.8",
|
| 282 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz",
|
| 283 |
+
"integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==",
|
| 284 |
+
"cpu": [
|
| 285 |
+
"s390x"
|
| 286 |
+
],
|
| 287 |
+
"dev": true,
|
| 288 |
+
"license": "MIT",
|
| 289 |
+
"optional": true,
|
| 290 |
+
"os": [
|
| 291 |
+
"linux"
|
| 292 |
+
],
|
| 293 |
+
"engines": {
|
| 294 |
+
"node": ">=18"
|
| 295 |
+
}
|
| 296 |
+
},
|
| 297 |
+
"node_modules/@esbuild/linux-x64": {
|
| 298 |
+
"version": "0.25.8",
|
| 299 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz",
|
| 300 |
+
"integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==",
|
| 301 |
+
"cpu": [
|
| 302 |
+
"x64"
|
| 303 |
+
],
|
| 304 |
+
"dev": true,
|
| 305 |
+
"license": "MIT",
|
| 306 |
+
"optional": true,
|
| 307 |
+
"os": [
|
| 308 |
+
"linux"
|
| 309 |
+
],
|
| 310 |
+
"engines": {
|
| 311 |
+
"node": ">=18"
|
| 312 |
+
}
|
| 313 |
+
},
|
| 314 |
+
"node_modules/@esbuild/netbsd-arm64": {
|
| 315 |
+
"version": "0.25.8",
|
| 316 |
+
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz",
|
| 317 |
+
"integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==",
|
| 318 |
+
"cpu": [
|
| 319 |
+
"arm64"
|
| 320 |
+
],
|
| 321 |
+
"dev": true,
|
| 322 |
+
"license": "MIT",
|
| 323 |
+
"optional": true,
|
| 324 |
+
"os": [
|
| 325 |
+
"netbsd"
|
| 326 |
+
],
|
| 327 |
+
"engines": {
|
| 328 |
+
"node": ">=18"
|
| 329 |
+
}
|
| 330 |
+
},
|
| 331 |
+
"node_modules/@esbuild/netbsd-x64": {
|
| 332 |
+
"version": "0.25.8",
|
| 333 |
+
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz",
|
| 334 |
+
"integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==",
|
| 335 |
+
"cpu": [
|
| 336 |
+
"x64"
|
| 337 |
+
],
|
| 338 |
+
"dev": true,
|
| 339 |
+
"license": "MIT",
|
| 340 |
+
"optional": true,
|
| 341 |
+
"os": [
|
| 342 |
+
"netbsd"
|
| 343 |
+
],
|
| 344 |
+
"engines": {
|
| 345 |
+
"node": ">=18"
|
| 346 |
+
}
|
| 347 |
+
},
|
| 348 |
+
"node_modules/@esbuild/openbsd-arm64": {
|
| 349 |
+
"version": "0.25.8",
|
| 350 |
+
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz",
|
| 351 |
+
"integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==",
|
| 352 |
+
"cpu": [
|
| 353 |
+
"arm64"
|
| 354 |
+
],
|
| 355 |
+
"dev": true,
|
| 356 |
+
"license": "MIT",
|
| 357 |
+
"optional": true,
|
| 358 |
+
"os": [
|
| 359 |
+
"openbsd"
|
| 360 |
+
],
|
| 361 |
+
"engines": {
|
| 362 |
+
"node": ">=18"
|
| 363 |
+
}
|
| 364 |
+
},
|
| 365 |
+
"node_modules/@esbuild/openbsd-x64": {
|
| 366 |
+
"version": "0.25.8",
|
| 367 |
+
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz",
|
| 368 |
+
"integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==",
|
| 369 |
+
"cpu": [
|
| 370 |
+
"x64"
|
| 371 |
+
],
|
| 372 |
+
"dev": true,
|
| 373 |
+
"license": "MIT",
|
| 374 |
+
"optional": true,
|
| 375 |
+
"os": [
|
| 376 |
+
"openbsd"
|
| 377 |
+
],
|
| 378 |
+
"engines": {
|
| 379 |
+
"node": ">=18"
|
| 380 |
+
}
|
| 381 |
+
},
|
| 382 |
+
"node_modules/@esbuild/openharmony-arm64": {
|
| 383 |
+
"version": "0.25.8",
|
| 384 |
+
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz",
|
| 385 |
+
"integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==",
|
| 386 |
+
"cpu": [
|
| 387 |
+
"arm64"
|
| 388 |
+
],
|
| 389 |
+
"dev": true,
|
| 390 |
+
"license": "MIT",
|
| 391 |
+
"optional": true,
|
| 392 |
+
"os": [
|
| 393 |
+
"openharmony"
|
| 394 |
+
],
|
| 395 |
+
"engines": {
|
| 396 |
+
"node": ">=18"
|
| 397 |
+
}
|
| 398 |
+
},
|
| 399 |
+
"node_modules/@esbuild/sunos-x64": {
|
| 400 |
+
"version": "0.25.8",
|
| 401 |
+
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz",
|
| 402 |
+
"integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==",
|
| 403 |
+
"cpu": [
|
| 404 |
+
"x64"
|
| 405 |
+
],
|
| 406 |
+
"dev": true,
|
| 407 |
+
"license": "MIT",
|
| 408 |
+
"optional": true,
|
| 409 |
+
"os": [
|
| 410 |
+
"sunos"
|
| 411 |
+
],
|
| 412 |
+
"engines": {
|
| 413 |
+
"node": ">=18"
|
| 414 |
+
}
|
| 415 |
+
},
|
| 416 |
+
"node_modules/@esbuild/win32-arm64": {
|
| 417 |
+
"version": "0.25.8",
|
| 418 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz",
|
| 419 |
+
"integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==",
|
| 420 |
+
"cpu": [
|
| 421 |
+
"arm64"
|
| 422 |
+
],
|
| 423 |
+
"dev": true,
|
| 424 |
+
"license": "MIT",
|
| 425 |
+
"optional": true,
|
| 426 |
+
"os": [
|
| 427 |
+
"win32"
|
| 428 |
+
],
|
| 429 |
+
"engines": {
|
| 430 |
+
"node": ">=18"
|
| 431 |
+
}
|
| 432 |
+
},
|
| 433 |
+
"node_modules/@esbuild/win32-ia32": {
|
| 434 |
+
"version": "0.25.8",
|
| 435 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz",
|
| 436 |
+
"integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==",
|
| 437 |
+
"cpu": [
|
| 438 |
+
"ia32"
|
| 439 |
+
],
|
| 440 |
+
"dev": true,
|
| 441 |
+
"license": "MIT",
|
| 442 |
+
"optional": true,
|
| 443 |
+
"os": [
|
| 444 |
+
"win32"
|
| 445 |
+
],
|
| 446 |
+
"engines": {
|
| 447 |
+
"node": ">=18"
|
| 448 |
+
}
|
| 449 |
+
},
|
| 450 |
+
"node_modules/@esbuild/win32-x64": {
|
| 451 |
+
"version": "0.25.8",
|
| 452 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz",
|
| 453 |
+
"integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==",
|
| 454 |
+
"cpu": [
|
| 455 |
+
"x64"
|
| 456 |
+
],
|
| 457 |
+
"dev": true,
|
| 458 |
+
"license": "MIT",
|
| 459 |
+
"optional": true,
|
| 460 |
+
"os": [
|
| 461 |
+
"win32"
|
| 462 |
+
],
|
| 463 |
+
"engines": {
|
| 464 |
+
"node": ">=18"
|
| 465 |
+
}
|
| 466 |
+
},
|
| 467 |
+
"node_modules/@hono/node-server": {
|
| 468 |
+
"version": "1.17.1",
|
| 469 |
+
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.17.1.tgz",
|
| 470 |
+
"integrity": "sha512-SY79W/C+2b1MyAzmIcV32Q47vO1b5XwLRwj8S9N6Jr5n1QCkIfAIH6umOSgqWZ4/v67hg6qq8Ha5vZonVidGsg==",
|
| 471 |
+
"license": "MIT",
|
| 472 |
+
"engines": {
|
| 473 |
+
"node": ">=18.14.1"
|
| 474 |
+
},
|
| 475 |
+
"peerDependencies": {
|
| 476 |
+
"hono": "^4"
|
| 477 |
+
}
|
| 478 |
+
},
|
| 479 |
+
"node_modules/@types/node": {
|
| 480 |
+
"version": "20.19.9",
|
| 481 |
+
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.9.tgz",
|
| 482 |
+
"integrity": "sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw==",
|
| 483 |
+
"dev": true,
|
| 484 |
+
"license": "MIT",
|
| 485 |
+
"dependencies": {
|
| 486 |
+
"undici-types": "~6.21.0"
|
| 487 |
+
}
|
| 488 |
+
},
|
| 489 |
+
"node_modules/@types/uuid": {
|
| 490 |
+
"version": "10.0.0",
|
| 491 |
+
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
|
| 492 |
+
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
|
| 493 |
+
"license": "MIT"
|
| 494 |
+
},
|
| 495 |
+
"node_modules/data-uri-to-buffer": {
|
| 496 |
+
"version": "4.0.1",
|
| 497 |
+
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
|
| 498 |
+
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
|
| 499 |
+
"license": "MIT",
|
| 500 |
+
"engines": {
|
| 501 |
+
"node": ">= 12"
|
| 502 |
+
}
|
| 503 |
+
},
|
| 504 |
+
"node_modules/dotenv": {
|
| 505 |
+
"version": "16.6.1",
|
| 506 |
+
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
| 507 |
+
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
|
| 508 |
+
"license": "BSD-2-Clause",
|
| 509 |
+
"engines": {
|
| 510 |
+
"node": ">=12"
|
| 511 |
+
},
|
| 512 |
+
"funding": {
|
| 513 |
+
"url": "https://dotenvx.com"
|
| 514 |
+
}
|
| 515 |
+
},
|
| 516 |
+
"node_modules/esbuild": {
|
| 517 |
+
"version": "0.25.8",
|
| 518 |
+
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz",
|
| 519 |
+
"integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==",
|
| 520 |
+
"dev": true,
|
| 521 |
+
"hasInstallScript": true,
|
| 522 |
+
"license": "MIT",
|
| 523 |
+
"bin": {
|
| 524 |
+
"esbuild": "bin/esbuild"
|
| 525 |
+
},
|
| 526 |
+
"engines": {
|
| 527 |
+
"node": ">=18"
|
| 528 |
+
},
|
| 529 |
+
"optionalDependencies": {
|
| 530 |
+
"@esbuild/aix-ppc64": "0.25.8",
|
| 531 |
+
"@esbuild/android-arm": "0.25.8",
|
| 532 |
+
"@esbuild/android-arm64": "0.25.8",
|
| 533 |
+
"@esbuild/android-x64": "0.25.8",
|
| 534 |
+
"@esbuild/darwin-arm64": "0.25.8",
|
| 535 |
+
"@esbuild/darwin-x64": "0.25.8",
|
| 536 |
+
"@esbuild/freebsd-arm64": "0.25.8",
|
| 537 |
+
"@esbuild/freebsd-x64": "0.25.8",
|
| 538 |
+
"@esbuild/linux-arm": "0.25.8",
|
| 539 |
+
"@esbuild/linux-arm64": "0.25.8",
|
| 540 |
+
"@esbuild/linux-ia32": "0.25.8",
|
| 541 |
+
"@esbuild/linux-loong64": "0.25.8",
|
| 542 |
+
"@esbuild/linux-mips64el": "0.25.8",
|
| 543 |
+
"@esbuild/linux-ppc64": "0.25.8",
|
| 544 |
+
"@esbuild/linux-riscv64": "0.25.8",
|
| 545 |
+
"@esbuild/linux-s390x": "0.25.8",
|
| 546 |
+
"@esbuild/linux-x64": "0.25.8",
|
| 547 |
+
"@esbuild/netbsd-arm64": "0.25.8",
|
| 548 |
+
"@esbuild/netbsd-x64": "0.25.8",
|
| 549 |
+
"@esbuild/openbsd-arm64": "0.25.8",
|
| 550 |
+
"@esbuild/openbsd-x64": "0.25.8",
|
| 551 |
+
"@esbuild/openharmony-arm64": "0.25.8",
|
| 552 |
+
"@esbuild/sunos-x64": "0.25.8",
|
| 553 |
+
"@esbuild/win32-arm64": "0.25.8",
|
| 554 |
+
"@esbuild/win32-ia32": "0.25.8",
|
| 555 |
+
"@esbuild/win32-x64": "0.25.8"
|
| 556 |
+
}
|
| 557 |
+
},
|
| 558 |
+
"node_modules/fetch-blob": {
|
| 559 |
+
"version": "3.2.0",
|
| 560 |
+
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
|
| 561 |
+
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
|
| 562 |
+
"funding": [
|
| 563 |
+
{
|
| 564 |
+
"type": "github",
|
| 565 |
+
"url": "https://github.com/sponsors/jimmywarting"
|
| 566 |
+
},
|
| 567 |
+
{
|
| 568 |
+
"type": "paypal",
|
| 569 |
+
"url": "https://paypal.me/jimmywarting"
|
| 570 |
+
}
|
| 571 |
+
],
|
| 572 |
+
"license": "MIT",
|
| 573 |
+
"dependencies": {
|
| 574 |
+
"node-domexception": "^1.0.0",
|
| 575 |
+
"web-streams-polyfill": "^3.0.3"
|
| 576 |
+
},
|
| 577 |
+
"engines": {
|
| 578 |
+
"node": "^12.20 || >= 14.13"
|
| 579 |
+
}
|
| 580 |
+
},
|
| 581 |
+
"node_modules/formdata-polyfill": {
|
| 582 |
+
"version": "4.0.10",
|
| 583 |
+
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
| 584 |
+
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
|
| 585 |
+
"license": "MIT",
|
| 586 |
+
"dependencies": {
|
| 587 |
+
"fetch-blob": "^3.1.2"
|
| 588 |
+
},
|
| 589 |
+
"engines": {
|
| 590 |
+
"node": ">=12.20.0"
|
| 591 |
+
}
|
| 592 |
+
},
|
| 593 |
+
"node_modules/fsevents": {
|
| 594 |
+
"version": "2.3.3",
|
| 595 |
+
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
| 596 |
+
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
| 597 |
+
"dev": true,
|
| 598 |
+
"hasInstallScript": true,
|
| 599 |
+
"license": "MIT",
|
| 600 |
+
"optional": true,
|
| 601 |
+
"os": [
|
| 602 |
+
"darwin"
|
| 603 |
+
],
|
| 604 |
+
"engines": {
|
| 605 |
+
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
| 606 |
+
}
|
| 607 |
+
},
|
| 608 |
+
"node_modules/get-tsconfig": {
|
| 609 |
+
"version": "4.10.1",
|
| 610 |
+
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz",
|
| 611 |
+
"integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==",
|
| 612 |
+
"dev": true,
|
| 613 |
+
"license": "MIT",
|
| 614 |
+
"dependencies": {
|
| 615 |
+
"resolve-pkg-maps": "^1.0.0"
|
| 616 |
+
},
|
| 617 |
+
"funding": {
|
| 618 |
+
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
| 619 |
+
}
|
| 620 |
+
},
|
| 621 |
+
"node_modules/hono": {
|
| 622 |
+
"version": "4.8.5",
|
| 623 |
+
"resolved": "https://registry.npmjs.org/hono/-/hono-4.8.5.tgz",
|
| 624 |
+
"integrity": "sha512-Up2cQbtNz1s111qpnnECdTGqSIUIhZJMLikdKkshebQSEBcoUKq6XJayLGqSZWidiH0zfHRCJqFu062Mz5UuRA==",
|
| 625 |
+
"license": "MIT",
|
| 626 |
+
"engines": {
|
| 627 |
+
"node": ">=16.9.0"
|
| 628 |
+
}
|
| 629 |
+
},
|
| 630 |
+
"node_modules/node-domexception": {
|
| 631 |
+
"version": "1.0.0",
|
| 632 |
+
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
| 633 |
+
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
| 634 |
+
"deprecated": "Use your platform's native DOMException instead",
|
| 635 |
+
"funding": [
|
| 636 |
+
{
|
| 637 |
+
"type": "github",
|
| 638 |
+
"url": "https://github.com/sponsors/jimmywarting"
|
| 639 |
+
},
|
| 640 |
+
{
|
| 641 |
+
"type": "github",
|
| 642 |
+
"url": "https://paypal.me/jimmywarting"
|
| 643 |
+
}
|
| 644 |
+
],
|
| 645 |
+
"license": "MIT",
|
| 646 |
+
"engines": {
|
| 647 |
+
"node": ">=10.5.0"
|
| 648 |
+
}
|
| 649 |
+
},
|
| 650 |
+
"node_modules/node-fetch": {
|
| 651 |
+
"version": "3.3.2",
|
| 652 |
+
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
|
| 653 |
+
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
|
| 654 |
+
"license": "MIT",
|
| 655 |
+
"dependencies": {
|
| 656 |
+
"data-uri-to-buffer": "^4.0.0",
|
| 657 |
+
"fetch-blob": "^3.1.4",
|
| 658 |
+
"formdata-polyfill": "^4.0.10"
|
| 659 |
+
},
|
| 660 |
+
"engines": {
|
| 661 |
+
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
| 662 |
+
},
|
| 663 |
+
"funding": {
|
| 664 |
+
"type": "opencollective",
|
| 665 |
+
"url": "https://opencollective.com/node-fetch"
|
| 666 |
+
}
|
| 667 |
+
},
|
| 668 |
+
"node_modules/resolve-pkg-maps": {
|
| 669 |
+
"version": "1.0.0",
|
| 670 |
+
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
| 671 |
+
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
| 672 |
+
"dev": true,
|
| 673 |
+
"license": "MIT",
|
| 674 |
+
"funding": {
|
| 675 |
+
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
| 676 |
+
}
|
| 677 |
+
},
|
| 678 |
+
"node_modules/tsx": {
|
| 679 |
+
"version": "4.20.3",
|
| 680 |
+
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.3.tgz",
|
| 681 |
+
"integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==",
|
| 682 |
+
"dev": true,
|
| 683 |
+
"license": "MIT",
|
| 684 |
+
"dependencies": {
|
| 685 |
+
"esbuild": "~0.25.0",
|
| 686 |
+
"get-tsconfig": "^4.7.5"
|
| 687 |
+
},
|
| 688 |
+
"bin": {
|
| 689 |
+
"tsx": "dist/cli.mjs"
|
| 690 |
+
},
|
| 691 |
+
"engines": {
|
| 692 |
+
"node": ">=18.0.0"
|
| 693 |
+
},
|
| 694 |
+
"optionalDependencies": {
|
| 695 |
+
"fsevents": "~2.3.3"
|
| 696 |
+
}
|
| 697 |
+
},
|
| 698 |
+
"node_modules/typescript": {
|
| 699 |
+
"version": "5.8.3",
|
| 700 |
+
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
| 701 |
+
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
| 702 |
+
"dev": true,
|
| 703 |
+
"license": "Apache-2.0",
|
| 704 |
+
"bin": {
|
| 705 |
+
"tsc": "bin/tsc",
|
| 706 |
+
"tsserver": "bin/tsserver"
|
| 707 |
+
},
|
| 708 |
+
"engines": {
|
| 709 |
+
"node": ">=14.17"
|
| 710 |
+
}
|
| 711 |
+
},
|
| 712 |
+
"node_modules/undici-types": {
|
| 713 |
+
"version": "6.21.0",
|
| 714 |
+
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
| 715 |
+
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
| 716 |
+
"dev": true,
|
| 717 |
+
"license": "MIT"
|
| 718 |
+
},
|
| 719 |
+
"node_modules/uuid": {
|
| 720 |
+
"version": "9.0.1",
|
| 721 |
+
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
|
| 722 |
+
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
|
| 723 |
+
"funding": [
|
| 724 |
+
"https://github.com/sponsors/broofa",
|
| 725 |
+
"https://github.com/sponsors/ctavan"
|
| 726 |
+
],
|
| 727 |
+
"license": "MIT",
|
| 728 |
+
"bin": {
|
| 729 |
+
"uuid": "dist/bin/uuid"
|
| 730 |
+
}
|
| 731 |
+
},
|
| 732 |
+
"node_modules/web-streams-polyfill": {
|
| 733 |
+
"version": "3.3.3",
|
| 734 |
+
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
| 735 |
+
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
|
| 736 |
+
"license": "MIT",
|
| 737 |
+
"engines": {
|
| 738 |
+
"node": ">= 8"
|
| 739 |
+
}
|
| 740 |
+
},
|
| 741 |
+
"node_modules/zod": {
|
| 742 |
+
"version": "3.25.76",
|
| 743 |
+
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
| 744 |
+
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
| 745 |
+
"license": "MIT",
|
| 746 |
+
"funding": {
|
| 747 |
+
"url": "https://github.com/sponsors/colinhacks"
|
| 748 |
+
}
|
| 749 |
+
}
|
| 750 |
+
}
|
| 751 |
+
}
|
hono-proxy/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "@colpali/proxy",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"description": "Hono proxy server for ColPali Vespa Visual Retrieval",
|
| 5 |
+
"main": "dist/index.js",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "tsx watch src/index.ts",
|
| 8 |
+
"build": "tsc",
|
| 9 |
+
"start": "node dist/index.js",
|
| 10 |
+
"start:tsx": "tsx src/index.ts"
|
| 11 |
+
},
|
| 12 |
+
"dependencies": {
|
| 13 |
+
"@hono/node-server": "^1.8.0",
|
| 14 |
+
"@types/uuid": "^10.0.0",
|
| 15 |
+
"dotenv": "^16.4.1",
|
| 16 |
+
"hono": "^4.0.0",
|
| 17 |
+
"node-fetch": "^3.3.2",
|
| 18 |
+
"uuid": "^9.0.1",
|
| 19 |
+
"zod": "^3.22.4"
|
| 20 |
+
},
|
| 21 |
+
"devDependencies": {
|
| 22 |
+
"@types/node": "^20.11.5",
|
| 23 |
+
"tsx": "^4.7.0",
|
| 24 |
+
"typescript": "^5.3.3"
|
| 25 |
+
}
|
| 26 |
+
}
|
hono-proxy/src/config/index.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { config as dotenvConfig } from 'dotenv';
|
| 2 |
+
import { z } from 'zod';
|
| 3 |
+
|
| 4 |
+
dotenvConfig();
|
| 5 |
+
|
| 6 |
+
const envSchema = z.object({
|
| 7 |
+
PORT: z.string().default('4025'),
|
| 8 |
+
BACKEND_URL: z.string().default('http://localhost:7860'),
|
| 9 |
+
VESPA_APP_URL: z.string().optional(),
|
| 10 |
+
VESPA_CERT_PATH: z.string().optional(),
|
| 11 |
+
VESPA_KEY_PATH: z.string().optional(),
|
| 12 |
+
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
|
| 13 |
+
CORS_ORIGIN: z.string().default('http://localhost:3000'),
|
| 14 |
+
CACHE_TTL: z.string().default('300'), // 5 minutes
|
| 15 |
+
ENABLE_CACHE: z.string().default('true'),
|
| 16 |
+
RATE_LIMIT_WINDOW: z.string().default('60000'), // 1 minute
|
| 17 |
+
RATE_LIMIT_MAX: z.string().default('100'),
|
| 18 |
+
});
|
| 19 |
+
|
| 20 |
+
const parsedEnv = envSchema.safeParse(process.env);
|
| 21 |
+
|
| 22 |
+
if (!parsedEnv.success) {
|
| 23 |
+
console.error('β Invalid environment variables:', parsedEnv.error.format());
|
| 24 |
+
process.exit(1);
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
export const config = {
|
| 28 |
+
port: parseInt(parsedEnv.data.PORT),
|
| 29 |
+
backendUrl: parsedEnv.data.BACKEND_URL,
|
| 30 |
+
vespaAppUrl: parsedEnv.data.VESPA_APP_URL,
|
| 31 |
+
vespaCertPath: parsedEnv.data.VESPA_CERT_PATH,
|
| 32 |
+
vespaKeyPath: parsedEnv.data.VESPA_KEY_PATH,
|
| 33 |
+
nodeEnv: parsedEnv.data.NODE_ENV,
|
| 34 |
+
corsOrigin: parsedEnv.data.CORS_ORIGIN,
|
| 35 |
+
cacheTTL: parseInt(parsedEnv.data.CACHE_TTL),
|
| 36 |
+
enableCache: parsedEnv.data.ENABLE_CACHE === 'true',
|
| 37 |
+
rateLimit: {
|
| 38 |
+
windowMs: parseInt(parsedEnv.data.RATE_LIMIT_WINDOW),
|
| 39 |
+
max: parseInt(parsedEnv.data.RATE_LIMIT_MAX),
|
| 40 |
+
},
|
| 41 |
+
};
|
| 42 |
+
|
| 43 |
+
export const isDev = config.nodeEnv === 'development';
|
| 44 |
+
export const isProd = config.nodeEnv === 'production';
|
hono-proxy/src/index.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { serve } from '@hono/node-server';
|
| 2 |
+
import { Hono } from 'hono';
|
| 3 |
+
import { compress } from 'hono/compress';
|
| 4 |
+
import { secureHeaders } from 'hono/secure-headers';
|
| 5 |
+
import { timeout } from 'hono/timeout';
|
| 6 |
+
import { config } from './config';
|
| 7 |
+
import { corsMiddleware } from './middleware/cors';
|
| 8 |
+
import { loggerMiddleware, requestIdMiddleware } from './middleware/logger';
|
| 9 |
+
import { rateLimitMiddleware } from './middleware/rateLimit';
|
| 10 |
+
import { api } from './routes/api';
|
| 11 |
+
import { backendApi } from './routes/backend-api';
|
| 12 |
+
import { healthApp } from './routes/health';
|
| 13 |
+
|
| 14 |
+
const app = new Hono();
|
| 15 |
+
|
| 16 |
+
// Global middleware
|
| 17 |
+
app.use('*', requestIdMiddleware);
|
| 18 |
+
app.use('*', loggerMiddleware);
|
| 19 |
+
app.use('*', corsMiddleware);
|
| 20 |
+
app.use('*', secureHeaders());
|
| 21 |
+
app.use('*', compress());
|
| 22 |
+
|
| 23 |
+
// Apply rate limiting to API routes only
|
| 24 |
+
app.use('/api/*', rateLimitMiddleware);
|
| 25 |
+
|
| 26 |
+
// Apply timeout to prevent hanging requests (30 seconds, except for SSE)
|
| 27 |
+
app.use('/api/*', async (c, next) => {
|
| 28 |
+
if (c.req.path === '/api/chat') {
|
| 29 |
+
// Skip timeout for SSE endpoints
|
| 30 |
+
await next();
|
| 31 |
+
} else {
|
| 32 |
+
return timeout(30000)(c, next);
|
| 33 |
+
}
|
| 34 |
+
});
|
| 35 |
+
|
| 36 |
+
// Mount routes - matching backend API structure at root level
|
| 37 |
+
app.route('/', backendApi);
|
| 38 |
+
|
| 39 |
+
// Also mount at /api for direct Next.js API access (optional)
|
| 40 |
+
app.route('/api', api);
|
| 41 |
+
|
| 42 |
+
// Health check
|
| 43 |
+
app.route('/health', healthApp);
|
| 44 |
+
|
| 45 |
+
// Root info endpoint
|
| 46 |
+
app.get('/info', (c) => {
|
| 47 |
+
return c.json({
|
| 48 |
+
name: 'ColPali Hono Proxy',
|
| 49 |
+
version: '1.0.0',
|
| 50 |
+
endpoints: {
|
| 51 |
+
// Backend-compatible endpoints (Python API format)
|
| 52 |
+
search: '/fetch_results',
|
| 53 |
+
fullImage: '/full_image',
|
| 54 |
+
suggestions: '/suggestions',
|
| 55 |
+
similarityMaps: '/get_sim_map',
|
| 56 |
+
chat: '/get-message',
|
| 57 |
+
// Direct API endpoints
|
| 58 |
+
apiSearch: '/api/colpali-search',
|
| 59 |
+
apiFullImage: '/api/full-image',
|
| 60 |
+
apiSuggestions: '/api/query-suggestions',
|
| 61 |
+
apiSimilarityMaps: '/api/similarity-maps',
|
| 62 |
+
apiChat: '/api/visual-rag-chat',
|
| 63 |
+
health: '/health',
|
| 64 |
+
},
|
| 65 |
+
});
|
| 66 |
+
});
|
| 67 |
+
|
| 68 |
+
// 404 handler
|
| 69 |
+
app.notFound((c) => {
|
| 70 |
+
return c.json({ error: 'Not found', path: c.req.path }, 404);
|
| 71 |
+
});
|
| 72 |
+
|
| 73 |
+
// Global error handler
|
| 74 |
+
app.onError((err, c) => {
|
| 75 |
+
console.error(`Error handling request ${c.req.path}:`, err);
|
| 76 |
+
|
| 77 |
+
if (err instanceof Error) {
|
| 78 |
+
if (err.message.includes('timeout')) {
|
| 79 |
+
return c.json({ error: 'Request timeout' }, 408);
|
| 80 |
+
}
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
return c.json(
|
| 84 |
+
{
|
| 85 |
+
error: 'Internal server error',
|
| 86 |
+
requestId: c.get('requestId'),
|
| 87 |
+
},
|
| 88 |
+
500
|
| 89 |
+
);
|
| 90 |
+
});
|
| 91 |
+
|
| 92 |
+
// Start server
|
| 93 |
+
const port = config.port;
|
| 94 |
+
|
| 95 |
+
console.log(`π ColPali Hono Proxy starting...`);
|
| 96 |
+
console.log(`π Backend URL: ${config.backendUrl}`);
|
| 97 |
+
console.log(`π CORS Origin: ${config.corsOrigin}`);
|
| 98 |
+
console.log(`πΎ Cache: ${config.enableCache ? 'Enabled' : 'Disabled'}`);
|
| 99 |
+
console.log(`π¦ Rate Limit: ${config.rateLimit.max} requests per ${config.rateLimit.windowMs / 1000}s`);
|
| 100 |
+
|
| 101 |
+
serve({
|
| 102 |
+
fetch: app.fetch,
|
| 103 |
+
port,
|
| 104 |
+
}, (info) => {
|
| 105 |
+
console.log(`β
Server running on http://localhost:${info.port}`);
|
| 106 |
+
});
|
hono-proxy/src/middleware/cors.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { cors as honoCors } from 'hono/cors';
|
| 2 |
+
import { config } from '../config';
|
| 3 |
+
|
| 4 |
+
export const corsMiddleware = honoCors({
|
| 5 |
+
origin: (origin) => {
|
| 6 |
+
// Allow configured origin and localhost in development
|
| 7 |
+
const allowedOrigins = [config.corsOrigin];
|
| 8 |
+
if (config.nodeEnv === 'development') {
|
| 9 |
+
allowedOrigins.push('http://localhost:3000', 'http://localhost:3001', 'http://localhost:3025');
|
| 10 |
+
}
|
| 11 |
+
return allowedOrigins.includes(origin) ? origin : allowedOrigins[0];
|
| 12 |
+
},
|
| 13 |
+
allowHeaders: ['Content-Type', 'Authorization', 'X-Request-ID'],
|
| 14 |
+
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
| 15 |
+
exposeHeaders: ['X-Total-Count', 'X-Request-ID'],
|
| 16 |
+
maxAge: 86400,
|
| 17 |
+
credentials: true,
|
| 18 |
+
});
|
hono-proxy/src/middleware/logger.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Context, Next } from 'hono';
|
| 2 |
+
import { logger } from 'hono/logger';
|
| 3 |
+
|
| 4 |
+
export const loggerMiddleware = logger((str, ...rest) => {
|
| 5 |
+
console.log(str, ...rest);
|
| 6 |
+
});
|
| 7 |
+
|
| 8 |
+
export const requestIdMiddleware = async (c: Context, next: Next) => {
|
| 9 |
+
const requestId = c.req.header('X-Request-ID') || crypto.randomUUID();
|
| 10 |
+
c.set('requestId', requestId);
|
| 11 |
+
c.header('X-Request-ID', requestId);
|
| 12 |
+
await next();
|
| 13 |
+
};
|
hono-proxy/src/middleware/rateLimit.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Context, Next } from 'hono';
|
| 2 |
+
import { config } from '../config';
|
| 3 |
+
|
| 4 |
+
interface RateLimitStore {
|
| 5 |
+
[key: string]: {
|
| 6 |
+
count: number;
|
| 7 |
+
resetTime: number;
|
| 8 |
+
};
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
const store: RateLimitStore = {};
|
| 12 |
+
|
| 13 |
+
// Simple in-memory rate limiter
|
| 14 |
+
export const rateLimitMiddleware = async (c: Context, next: Next) => {
|
| 15 |
+
const ip = c.req.header('x-forwarded-for') || c.req.header('x-real-ip') || 'unknown';
|
| 16 |
+
const now = Date.now();
|
| 17 |
+
const windowStart = now - config.rateLimit.windowMs;
|
| 18 |
+
|
| 19 |
+
// Clean up old entries
|
| 20 |
+
Object.keys(store).forEach(key => {
|
| 21 |
+
if (store[key].resetTime < windowStart) {
|
| 22 |
+
delete store[key];
|
| 23 |
+
}
|
| 24 |
+
});
|
| 25 |
+
|
| 26 |
+
// Check rate limit
|
| 27 |
+
if (!store[ip]) {
|
| 28 |
+
store[ip] = { count: 1, resetTime: now + config.rateLimit.windowMs };
|
| 29 |
+
} else if (store[ip].resetTime < now) {
|
| 30 |
+
store[ip] = { count: 1, resetTime: now + config.rateLimit.windowMs };
|
| 31 |
+
} else {
|
| 32 |
+
store[ip].count++;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
if (store[ip].count > config.rateLimit.max) {
|
| 36 |
+
return c.json(
|
| 37 |
+
{ error: 'Too many requests', retryAfter: Math.ceil((store[ip].resetTime - now) / 1000) },
|
| 38 |
+
429,
|
| 39 |
+
{
|
| 40 |
+
'Retry-After': Math.ceil((store[ip].resetTime - now) / 1000).toString(),
|
| 41 |
+
'X-RateLimit-Limit': config.rateLimit.max.toString(),
|
| 42 |
+
'X-RateLimit-Remaining': '0',
|
| 43 |
+
'X-RateLimit-Reset': new Date(store[ip].resetTime).toISOString(),
|
| 44 |
+
}
|
| 45 |
+
);
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
// Add rate limit headers
|
| 49 |
+
c.header('X-RateLimit-Limit', config.rateLimit.max.toString());
|
| 50 |
+
c.header('X-RateLimit-Remaining', (config.rateLimit.max - store[ip].count).toString());
|
| 51 |
+
c.header('X-RateLimit-Reset', new Date(store[ip].resetTime).toISOString());
|
| 52 |
+
|
| 53 |
+
await next();
|
| 54 |
+
};
|
hono-proxy/src/routes/api.ts
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Hono } from 'hono';
|
| 2 |
+
import { streamSSE } from 'hono/streaming';
|
| 3 |
+
import { v4 as uuidv4 } from 'uuid';
|
| 4 |
+
import { z } from 'zod';
|
| 5 |
+
import { config } from '../config';
|
| 6 |
+
import { cache } from '../services/cache';
|
| 7 |
+
import { vespaRequest } from '../services/vespa-https';
|
| 8 |
+
|
| 9 |
+
const api = new Hono();
|
| 10 |
+
|
| 11 |
+
// Search request schema
|
| 12 |
+
const searchQuerySchema = z.object({
|
| 13 |
+
query: z.string().min(1).max(500),
|
| 14 |
+
ranking: z.enum(['hybrid', 'colpali', 'bm25']).optional().default('hybrid'),
|
| 15 |
+
});
|
| 16 |
+
|
| 17 |
+
// Main search endpoint
|
| 18 |
+
api.get('/colpali-search', async (c) => {
|
| 19 |
+
try {
|
| 20 |
+
const query = c.req.query('query');
|
| 21 |
+
const ranking = c.req.query('ranking') || 'hybrid';
|
| 22 |
+
|
| 23 |
+
const validation = searchQuerySchema.safeParse({ query, ranking });
|
| 24 |
+
|
| 25 |
+
if (!validation.success) {
|
| 26 |
+
return c.json({ error: 'Invalid request', details: validation.error.issues }, 400);
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
const validatedData = validation.data;
|
| 30 |
+
|
| 31 |
+
// Check cache
|
| 32 |
+
const cacheKey = `search:${validatedData.query}:${validatedData.ranking}`;
|
| 33 |
+
const cachedResult = cache.get(cacheKey);
|
| 34 |
+
|
| 35 |
+
if (cachedResult) {
|
| 36 |
+
c.header('X-Cache', 'HIT');
|
| 37 |
+
return c.json(cachedResult);
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
// Build YQL query based on ranking
|
| 41 |
+
let yql = '';
|
| 42 |
+
let rankProfile = 'default';
|
| 43 |
+
|
| 44 |
+
switch (validatedData.ranking) {
|
| 45 |
+
case 'colpali':
|
| 46 |
+
yql = `select * from linqto where userQuery() limit 20`;
|
| 47 |
+
rankProfile = 'colpali';
|
| 48 |
+
break;
|
| 49 |
+
case 'bm25':
|
| 50 |
+
yql = `select * from linqto where userQuery() limit 20`;
|
| 51 |
+
rankProfile = 'bm25';
|
| 52 |
+
break;
|
| 53 |
+
case 'hybrid':
|
| 54 |
+
default:
|
| 55 |
+
yql = `select * from linqto where userQuery() limit 20`;
|
| 56 |
+
rankProfile = 'default';
|
| 57 |
+
break;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
// Query Vespa directly
|
| 61 |
+
const searchUrl = `${config.vespaAppUrl}/search/`;
|
| 62 |
+
const searchParams = new URLSearchParams({
|
| 63 |
+
yql,
|
| 64 |
+
query: validatedData.query,
|
| 65 |
+
ranking: rankProfile,
|
| 66 |
+
hits: '20'
|
| 67 |
+
});
|
| 68 |
+
|
| 69 |
+
const response = await vespaRequest(`${searchUrl}?${searchParams}`);
|
| 70 |
+
|
| 71 |
+
if (!response.ok) {
|
| 72 |
+
const errorText = await response.text();
|
| 73 |
+
console.error('Vespa error:', errorText);
|
| 74 |
+
throw new Error(`Vespa returned ${response.status}: ${errorText}`);
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
const data = await response.json();
|
| 78 |
+
|
| 79 |
+
// Generate query_id for sim_map compatibility
|
| 80 |
+
const queryId = uuidv4();
|
| 81 |
+
|
| 82 |
+
// Transform to match expected format
|
| 83 |
+
if (data.root && data.root.children) {
|
| 84 |
+
data.root.children.forEach((hit: any, idx: number) => {
|
| 85 |
+
if (!hit.fields) hit.fields = {};
|
| 86 |
+
// Add sim_map identifier for compatibility
|
| 87 |
+
hit.fields.sim_map = `${queryId}_${idx}`;
|
| 88 |
+
});
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
// Cache the result
|
| 92 |
+
cache.set(cacheKey, data);
|
| 93 |
+
c.header('X-Cache', 'MISS');
|
| 94 |
+
|
| 95 |
+
return c.json(data);
|
| 96 |
+
} catch (error) {
|
| 97 |
+
console.error('Search error:', error);
|
| 98 |
+
return c.json({
|
| 99 |
+
error: 'Search failed',
|
| 100 |
+
message: error instanceof Error ? error.message : 'Unknown error'
|
| 101 |
+
}, 500);
|
| 102 |
+
}
|
| 103 |
+
});
|
| 104 |
+
|
| 105 |
+
// Full image endpoint
|
| 106 |
+
api.get('/full-image', async (c) => {
|
| 107 |
+
try {
|
| 108 |
+
const docId = c.req.query('docId');
|
| 109 |
+
|
| 110 |
+
if (!docId) {
|
| 111 |
+
return c.json({ error: 'docId is required' }, 400);
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
// Check cache
|
| 115 |
+
const cacheKey = `fullimage:${docId}`;
|
| 116 |
+
const cachedImage = cache.get<{ base64_image: string }>(cacheKey);
|
| 117 |
+
|
| 118 |
+
if (cachedImage) {
|
| 119 |
+
c.header('X-Cache', 'HIT');
|
| 120 |
+
return c.json(cachedImage);
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
// Query Vespa for the document
|
| 124 |
+
const searchUrl = `${config.vespaAppUrl}/search/`;
|
| 125 |
+
const searchParams = new URLSearchParams({
|
| 126 |
+
yql: `select * from linqto where id contains "${docId}"`,
|
| 127 |
+
hits: '1'
|
| 128 |
+
});
|
| 129 |
+
|
| 130 |
+
const response = await vespaRequest(`${searchUrl}?${searchParams}`);
|
| 131 |
+
|
| 132 |
+
if (!response.ok) {
|
| 133 |
+
throw new Error(`Vespa returned ${response.status}`);
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
const data = await response.json();
|
| 137 |
+
|
| 138 |
+
if (data.root?.children?.[0]?.fields) {
|
| 139 |
+
const fields = data.root.children[0].fields;
|
| 140 |
+
const base64Image = fields.full_image || fields.image;
|
| 141 |
+
|
| 142 |
+
if (base64Image) {
|
| 143 |
+
const result = { base64_image: base64Image };
|
| 144 |
+
cache.set(cacheKey, result, 86400); // 24 hours
|
| 145 |
+
c.header('X-Cache', 'MISS');
|
| 146 |
+
return c.json(result);
|
| 147 |
+
}
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
return c.json({ error: 'Image not found' }, 404);
|
| 151 |
+
} catch (error) {
|
| 152 |
+
console.error('Full image error:', error);
|
| 153 |
+
return c.json({
|
| 154 |
+
error: 'Failed to fetch image',
|
| 155 |
+
message: error instanceof Error ? error.message : 'Unknown error'
|
| 156 |
+
}, 500);
|
| 157 |
+
}
|
| 158 |
+
});
|
| 159 |
+
|
| 160 |
+
// Query suggestions endpoint
|
| 161 |
+
api.get('/query-suggestions', async (c) => {
|
| 162 |
+
try {
|
| 163 |
+
const query = c.req.query('query');
|
| 164 |
+
|
| 165 |
+
// Static suggestions for now
|
| 166 |
+
const staticSuggestions = [
|
| 167 |
+
'linqto bankruptcy',
|
| 168 |
+
'linqto filing date',
|
| 169 |
+
'linqto creditors',
|
| 170 |
+
'linqto assets',
|
| 171 |
+
'linqto liabilities',
|
| 172 |
+
'linqto chapter 11',
|
| 173 |
+
'linqto docket',
|
| 174 |
+
'linqto plan',
|
| 175 |
+
'linqto disclosure statement',
|
| 176 |
+
'linqto claims',
|
| 177 |
+
];
|
| 178 |
+
|
| 179 |
+
if (!query) {
|
| 180 |
+
return c.json({ suggestions: staticSuggestions.slice(0, 5) });
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
const lowerQuery = query.toLowerCase();
|
| 184 |
+
const filtered = staticSuggestions
|
| 185 |
+
.filter(s => s.toLowerCase().includes(lowerQuery))
|
| 186 |
+
.slice(0, 5);
|
| 187 |
+
|
| 188 |
+
return c.json({ suggestions: filtered });
|
| 189 |
+
} catch (error) {
|
| 190 |
+
console.error('Suggestions error:', error);
|
| 191 |
+
return c.json({
|
| 192 |
+
error: 'Failed to fetch suggestions',
|
| 193 |
+
suggestions: []
|
| 194 |
+
}, 500);
|
| 195 |
+
}
|
| 196 |
+
});
|
| 197 |
+
|
| 198 |
+
// Similarity maps endpoint (placeholder)
|
| 199 |
+
api.get('/similarity-maps', async (c) => {
|
| 200 |
+
try {
|
| 201 |
+
const queryId = c.req.query('queryId');
|
| 202 |
+
const idx = c.req.query('idx');
|
| 203 |
+
const token = c.req.query('token');
|
| 204 |
+
const tokenIdx = c.req.query('tokenIdx');
|
| 205 |
+
|
| 206 |
+
if (!queryId || !idx || !token || !tokenIdx) {
|
| 207 |
+
return c.json({ error: 'Missing required parameters' }, 400);
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
// Return placeholder HTML
|
| 211 |
+
const html = `
|
| 212 |
+
<div style="padding: 20px; text-align: center;">
|
| 213 |
+
<h3>Similarity Map</h3>
|
| 214 |
+
<p>Query: ${token}</p>
|
| 215 |
+
<p>Document: ${idx}</p>
|
| 216 |
+
<p style="color: #666;">
|
| 217 |
+
Similarity map generation requires the ColPali model.
|
| 218 |
+
This is a placeholder for the demo.
|
| 219 |
+
</p>
|
| 220 |
+
</div>
|
| 221 |
+
`;
|
| 222 |
+
|
| 223 |
+
return c.html(html);
|
| 224 |
+
} catch (error) {
|
| 225 |
+
console.error('Similarity map error:', error);
|
| 226 |
+
return c.json({
|
| 227 |
+
error: 'Failed to generate similarity map',
|
| 228 |
+
message: error instanceof Error ? error.message : 'Unknown error'
|
| 229 |
+
}, 500);
|
| 230 |
+
}
|
| 231 |
+
});
|
| 232 |
+
|
| 233 |
+
// Visual RAG Chat SSE endpoint
|
| 234 |
+
api.get('/visual-rag-chat', async (c) => {
|
| 235 |
+
const queryId = c.req.query('queryId');
|
| 236 |
+
const query = c.req.query('query');
|
| 237 |
+
const docIds = c.req.query('docIds');
|
| 238 |
+
|
| 239 |
+
if (!queryId || !query || !docIds) {
|
| 240 |
+
return c.json({ error: 'Missing required parameters: queryId, query, docIds' }, 400);
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
return streamSSE(c, async (stream) => {
|
| 244 |
+
try {
|
| 245 |
+
// Mock response for now - in production this would use an LLM
|
| 246 |
+
const messages = [
|
| 247 |
+
`I'll analyze the search results for your query: "${query}"`,
|
| 248 |
+
"Based on the documents provided, here are the key findings:",
|
| 249 |
+
"1. LINQTO filed for Chapter 11 bankruptcy protection",
|
| 250 |
+
"2. The filing includes detailed financial statements and creditor information",
|
| 251 |
+
"3. Various claims and assets are documented in the court filings",
|
| 252 |
+
"",
|
| 253 |
+
"This is a demo response. In production, this would analyze the actual document contents using an LLM."
|
| 254 |
+
];
|
| 255 |
+
|
| 256 |
+
for (const msg of messages) {
|
| 257 |
+
await stream.writeSSE({ data: msg });
|
| 258 |
+
await new Promise(resolve => setTimeout(resolve, 300)); // Simulate typing
|
| 259 |
+
}
|
| 260 |
+
} catch (error) {
|
| 261 |
+
console.error('Chat streaming error:', error);
|
| 262 |
+
await stream.writeSSE({
|
| 263 |
+
event: 'error',
|
| 264 |
+
data: JSON.stringify({
|
| 265 |
+
error: 'Streaming failed',
|
| 266 |
+
message: error instanceof Error ? error.message : 'Unknown error'
|
| 267 |
+
}),
|
| 268 |
+
});
|
| 269 |
+
}
|
| 270 |
+
});
|
| 271 |
+
});
|
| 272 |
+
|
| 273 |
+
export { api };
|
| 274 |
+
|
hono-proxy/src/routes/backend-api.ts
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Hono } from 'hono';
|
| 2 |
+
import { streamSSE } from 'hono/streaming';
|
| 3 |
+
import { v4 as uuidv4 } from 'uuid';
|
| 4 |
+
import { z } from 'zod';
|
| 5 |
+
import { config } from '../config';
|
| 6 |
+
import { cache } from '../services/cache';
|
| 7 |
+
import { vespaRequest } from '../services/vespa-https';
|
| 8 |
+
|
| 9 |
+
const backendApi = new Hono();
|
| 10 |
+
|
| 11 |
+
// Search request schema
|
| 12 |
+
const searchQuerySchema = z.object({
|
| 13 |
+
query: z.string().min(1).max(500),
|
| 14 |
+
ranking: z.enum(['hybrid', 'colpali', 'bm25']).optional().default('hybrid'),
|
| 15 |
+
});
|
| 16 |
+
|
| 17 |
+
// Main search endpoint - /fetch_results
|
| 18 |
+
backendApi.get('/fetch_results', async (c) => {
|
| 19 |
+
try {
|
| 20 |
+
const query = c.req.query('query');
|
| 21 |
+
const ranking = c.req.query('ranking') || 'hybrid';
|
| 22 |
+
|
| 23 |
+
const validation = searchQuerySchema.safeParse({ query, ranking });
|
| 24 |
+
|
| 25 |
+
if (!validation.success) {
|
| 26 |
+
return c.json({ error: 'Invalid request', details: validation.error.issues }, 400);
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
const validatedData = validation.data;
|
| 30 |
+
|
| 31 |
+
// Check cache
|
| 32 |
+
const cacheKey = `search:${validatedData.query}:${validatedData.ranking}`;
|
| 33 |
+
const cachedResult = cache.get(cacheKey);
|
| 34 |
+
|
| 35 |
+
if (cachedResult) {
|
| 36 |
+
c.header('X-Cache', 'HIT');
|
| 37 |
+
return c.json(cachedResult);
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
// Build YQL query based on ranking
|
| 41 |
+
let yql = '';
|
| 42 |
+
let searchParams: any = {
|
| 43 |
+
query: validatedData.query,
|
| 44 |
+
hits: '20'
|
| 45 |
+
};
|
| 46 |
+
|
| 47 |
+
switch (validatedData.ranking) {
|
| 48 |
+
case 'colpali':
|
| 49 |
+
// Use retrieval-and-rerank profile for ColPali
|
| 50 |
+
yql = `select * from linqto where userQuery() limit 20`;
|
| 51 |
+
searchParams.ranking = 'retrieval-and-rerank';
|
| 52 |
+
break;
|
| 53 |
+
case 'bm25':
|
| 54 |
+
yql = `select * from linqto where userQuery() limit 20`;
|
| 55 |
+
searchParams.ranking = 'default';
|
| 56 |
+
break;
|
| 57 |
+
case 'hybrid':
|
| 58 |
+
default:
|
| 59 |
+
yql = `select * from linqto where userQuery() limit 20`;
|
| 60 |
+
searchParams.ranking = 'default';
|
| 61 |
+
break;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
// For ColPali ranking, we need embeddings
|
| 65 |
+
let body: any = {};
|
| 66 |
+
let useNearestNeighbor = false;
|
| 67 |
+
|
| 68 |
+
if (validatedData.ranking === 'colpali') {
|
| 69 |
+
try {
|
| 70 |
+
// Call embedding API to get query embeddings
|
| 71 |
+
const embeddingResponse = await fetch(
|
| 72 |
+
`http://localhost:7861/embed_query?query=${encodeURIComponent(validatedData.query)}`
|
| 73 |
+
);
|
| 74 |
+
|
| 75 |
+
if (embeddingResponse.ok) {
|
| 76 |
+
const embeddingData = await embeddingResponse.json();
|
| 77 |
+
|
| 78 |
+
// Create nearestNeighbor query string
|
| 79 |
+
const numTokens = Object.keys(embeddingData.embeddings.binary).length;
|
| 80 |
+
const maxTokens = Math.min(numTokens, 20); // Limit to 20 tokens to avoid timeouts
|
| 81 |
+
const nnClauses = [];
|
| 82 |
+
|
| 83 |
+
// Add individual rq tensors for nearestNeighbor
|
| 84 |
+
for (let i = 0; i < maxTokens; i++) {
|
| 85 |
+
body[`input.query(rq${i})`] = embeddingData.embeddings.binary[i.toString()];
|
| 86 |
+
nnClauses.push(`({targetHits:10}nearestNeighbor(embedding,rq${i}))`);
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
// Update YQL for nearestNeighbor search
|
| 90 |
+
if (nnClauses.length > 0) {
|
| 91 |
+
yql = `select * from linqto where ${nnClauses.join(' OR ')} limit 20`;
|
| 92 |
+
useNearestNeighbor = true;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
// Add qt and qtb for ranking
|
| 96 |
+
body["input.query(qt)"] = embeddingData.embeddings.float;
|
| 97 |
+
body["input.query(qtb)"] = embeddingData.embeddings.binary;
|
| 98 |
+
body["presentation.timing"] = true;
|
| 99 |
+
} else {
|
| 100 |
+
// Fall back to text-only search
|
| 101 |
+
searchParams.ranking = 'default';
|
| 102 |
+
}
|
| 103 |
+
} catch (error) {
|
| 104 |
+
console.log('Embedding API not available, falling back to text search');
|
| 105 |
+
searchParams.ranking = 'default';
|
| 106 |
+
}
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
// Query Vespa directly
|
| 110 |
+
const searchUrl = `${config.vespaAppUrl}/search/`;
|
| 111 |
+
const urlSearchParams = new URLSearchParams({
|
| 112 |
+
yql,
|
| 113 |
+
...searchParams
|
| 114 |
+
});
|
| 115 |
+
|
| 116 |
+
// Use ranking.profile for Vespa instead of ranking
|
| 117 |
+
if (searchParams.ranking) {
|
| 118 |
+
urlSearchParams.delete('ranking');
|
| 119 |
+
urlSearchParams.set('ranking.profile', searchParams.ranking);
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
const startTime = Date.now();
|
| 123 |
+
let requestOptions: any = {};
|
| 124 |
+
|
| 125 |
+
// Only use POST with body if we have embeddings
|
| 126 |
+
if (Object.keys(body).length > 0) {
|
| 127 |
+
requestOptions = {
|
| 128 |
+
method: 'POST',
|
| 129 |
+
headers: {
|
| 130 |
+
'Content-Type': 'application/json',
|
| 131 |
+
},
|
| 132 |
+
body: JSON.stringify(body)
|
| 133 |
+
};
|
| 134 |
+
} else {
|
| 135 |
+
requestOptions = {
|
| 136 |
+
method: 'GET'
|
| 137 |
+
};
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
console.log('Vespa query URL:', `${searchUrl}?${urlSearchParams}`);
|
| 141 |
+
console.log('Request options:', requestOptions);
|
| 142 |
+
|
| 143 |
+
const response = await vespaRequest(`${searchUrl}?${urlSearchParams}`, requestOptions);
|
| 144 |
+
|
| 145 |
+
if (!response.ok && response.status !== 504) {
|
| 146 |
+
const errorText = await response.text();
|
| 147 |
+
console.error('Vespa error:', errorText);
|
| 148 |
+
throw new Error(`Vespa returned ${response.status}: ${errorText}`);
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
const data = await response.json();
|
| 152 |
+
const searchTime = (Date.now() - startTime) / 1000; // Convert to seconds
|
| 153 |
+
|
| 154 |
+
// Generate query_id for sim_map compatibility
|
| 155 |
+
const queryId = uuidv4();
|
| 156 |
+
|
| 157 |
+
// Transform to match expected format
|
| 158 |
+
if (data.root && data.root.children) {
|
| 159 |
+
data.root.children.forEach((hit: any, idx: number) => {
|
| 160 |
+
if (!hit.fields) hit.fields = {};
|
| 161 |
+
// Add sim_map identifier for compatibility
|
| 162 |
+
hit.fields.sim_map = `${queryId}_${idx}`;
|
| 163 |
+
});
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
// Add timing information
|
| 167 |
+
data.timing = {
|
| 168 |
+
searchtime: searchTime
|
| 169 |
+
};
|
| 170 |
+
|
| 171 |
+
// Cache the result
|
| 172 |
+
cache.set(cacheKey, data);
|
| 173 |
+
c.header('X-Cache', 'MISS');
|
| 174 |
+
|
| 175 |
+
return c.json(data);
|
| 176 |
+
} catch (error) {
|
| 177 |
+
console.error('Search error:', error);
|
| 178 |
+
return c.json({
|
| 179 |
+
error: 'Search failed',
|
| 180 |
+
message: error instanceof Error ? error.message : 'Unknown error'
|
| 181 |
+
}, 500);
|
| 182 |
+
}
|
| 183 |
+
});
|
| 184 |
+
|
| 185 |
+
// Full image endpoint - /full_image
|
| 186 |
+
backendApi.get('/full_image', async (c) => {
|
| 187 |
+
try {
|
| 188 |
+
const docId = c.req.query('doc_id'); // Note: backend expects doc_id, not docId
|
| 189 |
+
|
| 190 |
+
if (!docId) {
|
| 191 |
+
return c.json({ error: 'doc_id is required' }, 400);
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
// Check cache
|
| 195 |
+
const cacheKey = `fullimage:${docId}`;
|
| 196 |
+
const cachedImage = cache.get<{ base64_image: string }>(cacheKey);
|
| 197 |
+
|
| 198 |
+
if (cachedImage) {
|
| 199 |
+
c.header('X-Cache', 'HIT');
|
| 200 |
+
return c.json(cachedImage);
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
// Query Vespa for the document
|
| 204 |
+
const searchUrl = `${config.vespaAppUrl}/search/`;
|
| 205 |
+
const searchParams = new URLSearchParams({
|
| 206 |
+
yql: `select * from linqto where id contains "${docId}"`,
|
| 207 |
+
hits: '1'
|
| 208 |
+
});
|
| 209 |
+
|
| 210 |
+
const response = await vespaRequest(`${searchUrl}?${searchParams}`);
|
| 211 |
+
|
| 212 |
+
if (!response.ok) {
|
| 213 |
+
throw new Error(`Vespa returned ${response.status}`);
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
const data = await response.json();
|
| 217 |
+
|
| 218 |
+
if (data.root?.children?.[0]?.fields) {
|
| 219 |
+
const fields = data.root.children[0].fields;
|
| 220 |
+
const base64Image = fields.full_image || fields.image;
|
| 221 |
+
|
| 222 |
+
if (base64Image) {
|
| 223 |
+
const result = { base64_image: base64Image };
|
| 224 |
+
cache.set(cacheKey, result, 86400); // 24 hours
|
| 225 |
+
c.header('X-Cache', 'MISS');
|
| 226 |
+
return c.json(result);
|
| 227 |
+
}
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
return c.json({ error: 'Image not found' }, 404);
|
| 231 |
+
} catch (error) {
|
| 232 |
+
console.error('Full image error:', error);
|
| 233 |
+
return c.json({
|
| 234 |
+
error: 'Failed to fetch image',
|
| 235 |
+
message: error instanceof Error ? error.message : 'Unknown error'
|
| 236 |
+
}, 500);
|
| 237 |
+
}
|
| 238 |
+
});
|
| 239 |
+
|
| 240 |
+
// Query suggestions endpoint - /suggestions
|
| 241 |
+
backendApi.get('/suggestions', async (c) => {
|
| 242 |
+
try {
|
| 243 |
+
const query = c.req.query('query') || '';
|
| 244 |
+
|
| 245 |
+
// Static suggestions for now
|
| 246 |
+
const staticSuggestions = [
|
| 247 |
+
'linqto bankruptcy',
|
| 248 |
+
'linqto filing date',
|
| 249 |
+
'linqto creditors',
|
| 250 |
+
'linqto assets',
|
| 251 |
+
'linqto liabilities',
|
| 252 |
+
'linqto chapter 11',
|
| 253 |
+
'linqto docket',
|
| 254 |
+
'linqto plan',
|
| 255 |
+
'linqto disclosure statement',
|
| 256 |
+
'linqto claims',
|
| 257 |
+
];
|
| 258 |
+
|
| 259 |
+
if (!query) {
|
| 260 |
+
return c.json({ suggestions: staticSuggestions.slice(0, 5) });
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
const lowerQuery = query.toLowerCase();
|
| 264 |
+
const filtered = staticSuggestions
|
| 265 |
+
.filter(s => s.startsWith(lowerQuery))
|
| 266 |
+
.slice(0, 5);
|
| 267 |
+
|
| 268 |
+
return c.json({ suggestions: filtered });
|
| 269 |
+
} catch (error) {
|
| 270 |
+
console.error('Suggestions error:', error);
|
| 271 |
+
return c.json({
|
| 272 |
+
error: 'Failed to fetch suggestions',
|
| 273 |
+
suggestions: []
|
| 274 |
+
}, 500);
|
| 275 |
+
}
|
| 276 |
+
});
|
| 277 |
+
|
| 278 |
+
// Similarity maps endpoint - /get_sim_map
|
| 279 |
+
backendApi.get('/get_sim_map', async (c) => {
|
| 280 |
+
try {
|
| 281 |
+
const queryId = c.req.query('query_id'); // Note: backend expects query_id
|
| 282 |
+
const idx = c.req.query('idx');
|
| 283 |
+
const token = c.req.query('token');
|
| 284 |
+
const tokenIdx = c.req.query('token_idx'); // Note: backend expects token_idx
|
| 285 |
+
|
| 286 |
+
if (!queryId || !idx || !token || !tokenIdx) {
|
| 287 |
+
return c.json({ error: 'Missing required parameters' }, 400);
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
// Return placeholder HTML
|
| 291 |
+
const html = `
|
| 292 |
+
<div style="padding: 20px; text-align: center;">
|
| 293 |
+
<h3>Similarity Map</h3>
|
| 294 |
+
<p>Query: ${token}</p>
|
| 295 |
+
<p>Document: ${idx}</p>
|
| 296 |
+
<p style="color: #666;">
|
| 297 |
+
Similarity map generation requires the ColPali model.
|
| 298 |
+
This is a placeholder for the demo.
|
| 299 |
+
</p>
|
| 300 |
+
</div>
|
| 301 |
+
`;
|
| 302 |
+
|
| 303 |
+
return c.html(html);
|
| 304 |
+
} catch (error) {
|
| 305 |
+
console.error('Similarity map error:', error);
|
| 306 |
+
return c.json({
|
| 307 |
+
error: 'Failed to generate similarity map',
|
| 308 |
+
message: error instanceof Error ? error.message : 'Unknown error'
|
| 309 |
+
}, 500);
|
| 310 |
+
}
|
| 311 |
+
});
|
| 312 |
+
|
| 313 |
+
// Visual RAG Chat SSE endpoint - /get-message
|
| 314 |
+
backendApi.get('/get-message', async (c) => {
|
| 315 |
+
const queryId = c.req.query('query_id'); // Note: backend expects query_id
|
| 316 |
+
const query = c.req.query('query');
|
| 317 |
+
const docIds = c.req.query('doc_ids'); // Note: backend expects doc_ids
|
| 318 |
+
|
| 319 |
+
if (!queryId || !query || !docIds) {
|
| 320 |
+
return c.json({ error: 'Missing required parameters: query_id, query, doc_ids' }, 400);
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
return streamSSE(c, async (stream) => {
|
| 324 |
+
try {
|
| 325 |
+
// Mock response for now - in production this would use an LLM
|
| 326 |
+
// Extract key information from the query
|
| 327 |
+
const messages = [];
|
| 328 |
+
|
| 329 |
+
if (query.toLowerCase().includes('when') && query.toLowerCase().includes('file')) {
|
| 330 |
+
messages.push(
|
| 331 |
+
`I'll analyze the search results for your query: "${query}"`,
|
| 332 |
+
"",
|
| 333 |
+
"Based on the documents provided:",
|
| 334 |
+
"",
|
| 335 |
+
"**LINQTO filed for Chapter 11 bankruptcy on July 7, 2025**",
|
| 336 |
+
"",
|
| 337 |
+
"The filing was made in the United States Bankruptcy Court for the Southern District of Texas under case number 25-90186.",
|
| 338 |
+
"",
|
| 339 |
+
"Key details:",
|
| 340 |
+
"β’ Filing Date: July 7, 2025 (Petition Date)",
|
| 341 |
+
"β’ Court: Southern District of Texas",
|
| 342 |
+
"β’ Case Number: 25-90186",
|
| 343 |
+
"β’ Chapter: 11 (Reorganization)",
|
| 344 |
+
"",
|
| 345 |
+
"This is a demo response. In production, an LLM would analyze the actual document contents for more details."
|
| 346 |
+
);
|
| 347 |
+
} else {
|
| 348 |
+
messages.push(
|
| 349 |
+
`I'll analyze the search results for your query: "${query}"`,
|
| 350 |
+
"Based on the documents provided, here are the key findings:",
|
| 351 |
+
"1. LINQTO filed for Chapter 11 bankruptcy protection on July 7, 2025",
|
| 352 |
+
"2. The filing includes detailed financial statements and creditor information",
|
| 353 |
+
"3. Various claims and assets are documented in the court filings",
|
| 354 |
+
"",
|
| 355 |
+
"This is a demo response. In production, this would analyze the actual document contents using an LLM."
|
| 356 |
+
);
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
for (const msg of messages) {
|
| 360 |
+
await stream.writeSSE({ data: msg });
|
| 361 |
+
await new Promise(resolve => setTimeout(resolve, 500)); // Simulate typing delay
|
| 362 |
+
}
|
| 363 |
+
} catch (error) {
|
| 364 |
+
console.error('Chat streaming error:', error);
|
| 365 |
+
await stream.writeSSE({
|
| 366 |
+
event: 'error',
|
| 367 |
+
data: JSON.stringify({
|
| 368 |
+
error: 'Streaming failed',
|
| 369 |
+
message: error instanceof Error ? error.message : 'Unknown error'
|
| 370 |
+
}),
|
| 371 |
+
});
|
| 372 |
+
}
|
| 373 |
+
});
|
| 374 |
+
});
|
| 375 |
+
|
| 376 |
+
export { backendApi };
|
hono-proxy/src/routes/chat-direct.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Hono } from 'hono';
|
| 2 |
+
import { streamSSE } from 'hono/streaming';
|
| 3 |
+
|
| 4 |
+
const chatApp = new Hono();
|
| 5 |
+
|
| 6 |
+
// Visual RAG Chat SSE endpoint
|
| 7 |
+
chatApp.get('/', async (c) => {
|
| 8 |
+
const queryId = c.req.query('queryId');
|
| 9 |
+
const query = c.req.query('query');
|
| 10 |
+
const docIds = c.req.query('docIds');
|
| 11 |
+
|
| 12 |
+
if (!queryId || !query || !docIds) {
|
| 13 |
+
return c.json({ error: 'Missing required parameters: queryId, query, docIds' }, 400);
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
return streamSSE(c, async (stream) => {
|
| 17 |
+
try {
|
| 18 |
+
// Mock response for now - in production this would use an LLM
|
| 19 |
+
const messages = [
|
| 20 |
+
`I'll analyze the search results for your query: "${query}"`,
|
| 21 |
+
"Based on the documents provided, here are the key findings:",
|
| 22 |
+
"1. LINQTO filed for Chapter 11 bankruptcy protection",
|
| 23 |
+
"2. The filing includes detailed financial statements and creditor information",
|
| 24 |
+
"3. Various claims and assets are documented in the court filings",
|
| 25 |
+
"",
|
| 26 |
+
"This is a demo response. In production, this would analyze the actual document contents using an LLM."
|
| 27 |
+
];
|
| 28 |
+
|
| 29 |
+
for (const msg of messages) {
|
| 30 |
+
await stream.writeSSE({ data: msg });
|
| 31 |
+
await new Promise(resolve => setTimeout(resolve, 300)); // Simulate typing
|
| 32 |
+
}
|
| 33 |
+
} catch (error) {
|
| 34 |
+
console.error('Chat streaming error:', error);
|
| 35 |
+
await stream.writeSSE({
|
| 36 |
+
event: 'error',
|
| 37 |
+
data: JSON.stringify({
|
| 38 |
+
error: 'Streaming failed',
|
| 39 |
+
message: error instanceof Error ? error.message : 'Unknown error'
|
| 40 |
+
}),
|
| 41 |
+
});
|
| 42 |
+
}
|
| 43 |
+
});
|
| 44 |
+
});
|
| 45 |
+
|
| 46 |
+
export { chatApp };
|
hono-proxy/src/routes/chat.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Hono } from 'hono';
|
| 2 |
+
import { streamSSE } from 'hono/streaming';
|
| 3 |
+
import { config } from '../config';
|
| 4 |
+
|
| 5 |
+
const chatApp = new Hono();
|
| 6 |
+
|
| 7 |
+
// Visual RAG Chat SSE endpoint - matches Next.js /api/visual-rag-chat
|
| 8 |
+
chatApp.get('/', async (c) => {
|
| 9 |
+
const queryId = c.req.query('queryId');
|
| 10 |
+
const query = c.req.query('query');
|
| 11 |
+
const docIds = c.req.query('docIds');
|
| 12 |
+
|
| 13 |
+
if (!queryId || !query || !docIds) {
|
| 14 |
+
return c.json({ error: 'Missing required parameters: queryId, query, docIds' }, 400);
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
return streamSSE(c, async (stream) => {
|
| 18 |
+
try {
|
| 19 |
+
// Create abort controller for cleanup
|
| 20 |
+
const abortController = new AbortController();
|
| 21 |
+
|
| 22 |
+
// Forward request to backend /get-message endpoint
|
| 23 |
+
const chatUrl = `${config.backendUrl}/get-message?query_id=${encodeURIComponent(queryId)}&query=${encodeURIComponent(query)}&doc_ids=${encodeURIComponent(docIds)}`;
|
| 24 |
+
|
| 25 |
+
const response = await fetch(chatUrl, {
|
| 26 |
+
headers: {
|
| 27 |
+
'Accept': 'text/event-stream',
|
| 28 |
+
},
|
| 29 |
+
signal: abortController.signal,
|
| 30 |
+
});
|
| 31 |
+
|
| 32 |
+
if (!response.ok) {
|
| 33 |
+
await stream.writeSSE({
|
| 34 |
+
event: 'error',
|
| 35 |
+
data: JSON.stringify({ error: `Backend returned ${response.status}` }),
|
| 36 |
+
});
|
| 37 |
+
return;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
if (!response.body) {
|
| 41 |
+
await stream.writeSSE({
|
| 42 |
+
event: 'error',
|
| 43 |
+
data: JSON.stringify({ error: 'No response body' }),
|
| 44 |
+
});
|
| 45 |
+
return;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
// Stream the response
|
| 49 |
+
const reader = response.body.getReader();
|
| 50 |
+
const decoder = new TextDecoder();
|
| 51 |
+
let buffer = '';
|
| 52 |
+
|
| 53 |
+
while (true) {
|
| 54 |
+
const { done, value } = await reader.read();
|
| 55 |
+
|
| 56 |
+
if (done) break;
|
| 57 |
+
|
| 58 |
+
buffer += decoder.decode(value, { stream: true });
|
| 59 |
+
const lines = buffer.split('\n');
|
| 60 |
+
|
| 61 |
+
// Keep the last incomplete line in the buffer
|
| 62 |
+
buffer = lines.pop() || '';
|
| 63 |
+
|
| 64 |
+
for (const line of lines) {
|
| 65 |
+
if (line.trim() === '') continue;
|
| 66 |
+
|
| 67 |
+
if (line.startsWith('data: ')) {
|
| 68 |
+
const data = line.slice(6);
|
| 69 |
+
await stream.writeSSE({ data });
|
| 70 |
+
} else if (line.startsWith('event: ')) {
|
| 71 |
+
// Handle event lines if backend sends them
|
| 72 |
+
const event = line.slice(7).trim();
|
| 73 |
+
// Look for the next data line
|
| 74 |
+
const nextLineIndex = lines.indexOf(line) + 1;
|
| 75 |
+
if (nextLineIndex < lines.length) {
|
| 76 |
+
const nextLine = lines[nextLineIndex];
|
| 77 |
+
if (nextLine.startsWith('data: ')) {
|
| 78 |
+
const data = nextLine.slice(6);
|
| 79 |
+
await stream.writeSSE({ event, data });
|
| 80 |
+
lines.splice(nextLineIndex, 1); // Remove processed line
|
| 81 |
+
}
|
| 82 |
+
}
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
// Handle any remaining data in buffer
|
| 88 |
+
if (buffer.trim()) {
|
| 89 |
+
if (buffer.startsWith('data: ')) {
|
| 90 |
+
await stream.writeSSE({ data: buffer.slice(6) });
|
| 91 |
+
}
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
// Cleanup
|
| 95 |
+
abortController.abort();
|
| 96 |
+
} catch (error) {
|
| 97 |
+
console.error('Chat streaming error:', error);
|
| 98 |
+
await stream.writeSSE({
|
| 99 |
+
event: 'error',
|
| 100 |
+
data: JSON.stringify({
|
| 101 |
+
error: 'Streaming failed',
|
| 102 |
+
message: error instanceof Error ? error.message : 'Unknown error'
|
| 103 |
+
}),
|
| 104 |
+
});
|
| 105 |
+
}
|
| 106 |
+
});
|
| 107 |
+
});
|
| 108 |
+
|
| 109 |
+
export { chatApp };
|
hono-proxy/src/routes/colpali-search-vespa.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Hono } from 'hono';
|
| 2 |
+
import { z } from 'zod';
|
| 3 |
+
import { config } from '../config';
|
| 4 |
+
import { cache } from '../services/cache';
|
| 5 |
+
|
| 6 |
+
const colpaliSearchApp = new Hono();
|
| 7 |
+
|
| 8 |
+
// Search request schema
|
| 9 |
+
const searchQuerySchema = z.object({
|
| 10 |
+
query: z.string().min(1).max(500),
|
| 11 |
+
ranking: z.enum(['hybrid', 'colpali', 'bm25']).optional().default('hybrid'),
|
| 12 |
+
});
|
| 13 |
+
|
| 14 |
+
// Main search endpoint - direct to Vespa
|
| 15 |
+
colpaliSearchApp.get('/', async (c) => {
|
| 16 |
+
try {
|
| 17 |
+
const query = c.req.query('query');
|
| 18 |
+
const ranking = c.req.query('ranking') || 'hybrid';
|
| 19 |
+
|
| 20 |
+
const validation = searchQuerySchema.safeParse({ query, ranking });
|
| 21 |
+
|
| 22 |
+
if (!validation.success) {
|
| 23 |
+
return c.json({ error: 'Invalid request', details: validation.error.issues }, 400);
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
const validatedData = validation.data;
|
| 27 |
+
|
| 28 |
+
// Check cache
|
| 29 |
+
const cacheKey = `search:${validatedData.query}:${validatedData.ranking}`;
|
| 30 |
+
const cachedResult = cache.get(cacheKey);
|
| 31 |
+
|
| 32 |
+
if (cachedResult) {
|
| 33 |
+
c.header('X-Cache', 'HIT');
|
| 34 |
+
return c.json(cachedResult);
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
// Prepare YQL query based on ranking type
|
| 38 |
+
let yql = '';
|
| 39 |
+
switch (validatedData.ranking) {
|
| 40 |
+
case 'colpali':
|
| 41 |
+
yql = `select * from linqto where userQuery() limit 20`;
|
| 42 |
+
break;
|
| 43 |
+
case 'bm25':
|
| 44 |
+
yql = `select * from linqto where userQuery() order by bm25_score desc limit 20`;
|
| 45 |
+
break;
|
| 46 |
+
case 'hybrid':
|
| 47 |
+
default:
|
| 48 |
+
yql = `select * from linqto where userQuery() | rank (reciprocal_rank_fusion(bm25_score, max_sim)) limit 20`;
|
| 49 |
+
break;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
// Query Vespa directly
|
| 53 |
+
const searchUrl = `${config.vespaAppUrl}/search/`;
|
| 54 |
+
const searchParams = new URLSearchParams({
|
| 55 |
+
yql,
|
| 56 |
+
query: validatedData.query,
|
| 57 |
+
ranking: validatedData.ranking === 'colpali' ? 'colpali' : 'default',
|
| 58 |
+
'summary': 'default',
|
| 59 |
+
'format': 'json'
|
| 60 |
+
});
|
| 61 |
+
|
| 62 |
+
// For now, using direct fetch without certificate authentication
|
| 63 |
+
// In production, you would use a proxy or configure certificates properly
|
| 64 |
+
const response = await fetch(`${searchUrl}?${searchParams}`, {
|
| 65 |
+
method: 'GET',
|
| 66 |
+
headers: {
|
| 67 |
+
'Accept': 'application/json',
|
| 68 |
+
}
|
| 69 |
+
});
|
| 70 |
+
|
| 71 |
+
if (!response.ok) {
|
| 72 |
+
throw new Error(`Vespa returned ${response.status}`);
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
const data = await response.json();
|
| 76 |
+
|
| 77 |
+
// Transform to match expected format (add sim_map if needed)
|
| 78 |
+
const transformedData = {
|
| 79 |
+
...data,
|
| 80 |
+
root: {
|
| 81 |
+
...data.root,
|
| 82 |
+
children: data.root?.children?.map((hit: any, idx: number) => ({
|
| 83 |
+
...hit,
|
| 84 |
+
fields: {
|
| 85 |
+
...hit.fields,
|
| 86 |
+
// Add sim_map field if not present (for compatibility)
|
| 87 |
+
sim_map: hit.fields.sim_map || `sim_map_${idx}`,
|
| 88 |
+
}
|
| 89 |
+
})) || []
|
| 90 |
+
}
|
| 91 |
+
};
|
| 92 |
+
|
| 93 |
+
// Cache the result
|
| 94 |
+
cache.set(cacheKey, transformedData);
|
| 95 |
+
c.header('X-Cache', 'MISS');
|
| 96 |
+
|
| 97 |
+
return c.json(transformedData);
|
| 98 |
+
} catch (error) {
|
| 99 |
+
console.error('Search error:', error);
|
| 100 |
+
return c.json({
|
| 101 |
+
error: 'Search failed',
|
| 102 |
+
message: error instanceof Error ? error.message : 'Unknown error'
|
| 103 |
+
}, 500);
|
| 104 |
+
}
|
| 105 |
+
});
|
| 106 |
+
|
| 107 |
+
export { colpaliSearchApp };
|
hono-proxy/src/routes/colpali-search.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Hono } from 'hono';
|
| 2 |
+
import { z } from 'zod';
|
| 3 |
+
import { config } from '../config';
|
| 4 |
+
import { cache } from '../services/cache';
|
| 5 |
+
|
| 6 |
+
const colpaliSearchApp = new Hono();
|
| 7 |
+
|
| 8 |
+
// Search request schema for GET requests
|
| 9 |
+
const searchQuerySchema = z.object({
|
| 10 |
+
query: z.string().min(1).max(500),
|
| 11 |
+
ranking: z.enum(['hybrid', 'colpali', 'bm25']).optional().default('hybrid'),
|
| 12 |
+
});
|
| 13 |
+
|
| 14 |
+
// Main search endpoint - matches Next.js /api/colpali-search
|
| 15 |
+
colpaliSearchApp.get('/', async (c) => {
|
| 16 |
+
try {
|
| 17 |
+
const query = c.req.query('query');
|
| 18 |
+
const ranking = c.req.query('ranking') || 'hybrid';
|
| 19 |
+
|
| 20 |
+
const validation = searchQuerySchema.safeParse({ query, ranking });
|
| 21 |
+
|
| 22 |
+
if (!validation.success) {
|
| 23 |
+
return c.json({ error: 'Invalid request', details: validation.error.issues }, 400);
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
const validatedData = validation.data;
|
| 27 |
+
|
| 28 |
+
// Check cache
|
| 29 |
+
const cacheKey = `search:${validatedData.query}:${validatedData.ranking}`;
|
| 30 |
+
const cachedResult = cache.get(cacheKey);
|
| 31 |
+
|
| 32 |
+
if (cachedResult) {
|
| 33 |
+
c.header('X-Cache', 'HIT');
|
| 34 |
+
return c.json(cachedResult);
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
// Proxy to backend /fetch_results endpoint
|
| 38 |
+
const searchUrl = `${config.backendUrl}/fetch_results?query=${encodeURIComponent(validatedData.query)}&ranking=${validatedData.ranking}`;
|
| 39 |
+
const response = await fetch(searchUrl);
|
| 40 |
+
|
| 41 |
+
if (!response.ok) {
|
| 42 |
+
throw new Error(`Backend returned ${response.status}`);
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
const data = await response.json();
|
| 46 |
+
|
| 47 |
+
// Cache the result
|
| 48 |
+
cache.set(cacheKey, data);
|
| 49 |
+
c.header('X-Cache', 'MISS');
|
| 50 |
+
|
| 51 |
+
return c.json(data);
|
| 52 |
+
} catch (error) {
|
| 53 |
+
console.error('Search error:', error);
|
| 54 |
+
return c.json({
|
| 55 |
+
error: 'Search failed',
|
| 56 |
+
message: error instanceof Error ? error.message : 'Unknown error'
|
| 57 |
+
}, 500);
|
| 58 |
+
}
|
| 59 |
+
});
|
| 60 |
+
|
| 61 |
+
export { colpaliSearchApp };
|
hono-proxy/src/routes/full-image.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Hono } from 'hono';
|
| 2 |
+
import { config } from '../config';
|
| 3 |
+
import { cache } from '../services/cache';
|
| 4 |
+
|
| 5 |
+
const fullImageApp = new Hono();
|
| 6 |
+
|
| 7 |
+
// Full image endpoint - matches Next.js /api/full-image
|
| 8 |
+
fullImageApp.get('/', async (c) => {
|
| 9 |
+
try {
|
| 10 |
+
const docId = c.req.query('docId');
|
| 11 |
+
|
| 12 |
+
if (!docId) {
|
| 13 |
+
return c.json({ error: 'docId is required' }, 400);
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
// Check cache
|
| 17 |
+
const cacheKey = `fullimage:${docId}`;
|
| 18 |
+
const cachedImage = cache.get<{ base64_image: string }>(cacheKey);
|
| 19 |
+
|
| 20 |
+
if (cachedImage) {
|
| 21 |
+
c.header('X-Cache', 'HIT');
|
| 22 |
+
return c.json(cachedImage);
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
// Proxy to backend
|
| 26 |
+
const imageUrl = `${config.backendUrl}/full_image?doc_id=${encodeURIComponent(docId)}`;
|
| 27 |
+
const response = await fetch(imageUrl);
|
| 28 |
+
|
| 29 |
+
if (!response.ok) {
|
| 30 |
+
throw new Error(`Backend returned ${response.status}`);
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
const data = await response.json();
|
| 34 |
+
|
| 35 |
+
// Cache for 24 hours
|
| 36 |
+
cache.set(cacheKey, data, 86400);
|
| 37 |
+
c.header('X-Cache', 'MISS');
|
| 38 |
+
|
| 39 |
+
return c.json(data);
|
| 40 |
+
} catch (error) {
|
| 41 |
+
console.error('Full image error:', error);
|
| 42 |
+
return c.json({
|
| 43 |
+
error: 'Failed to fetch image',
|
| 44 |
+
message: error instanceof Error ? error.message : 'Unknown error'
|
| 45 |
+
}, 500);
|
| 46 |
+
}
|
| 47 |
+
});
|
| 48 |
+
|
| 49 |
+
export { fullImageApp };
|
hono-proxy/src/routes/health.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Hono } from 'hono';
|
| 2 |
+
import { config } from '../config';
|
| 3 |
+
|
| 4 |
+
const healthApp = new Hono();
|
| 5 |
+
|
| 6 |
+
interface HealthStatus {
|
| 7 |
+
status: 'healthy' | 'degraded' | 'unhealthy';
|
| 8 |
+
timestamp: string;
|
| 9 |
+
uptime: number;
|
| 10 |
+
services: {
|
| 11 |
+
backend: {
|
| 12 |
+
status: 'up' | 'down';
|
| 13 |
+
responseTime?: number;
|
| 14 |
+
error?: string;
|
| 15 |
+
};
|
| 16 |
+
cache: {
|
| 17 |
+
status: 'up' | 'down';
|
| 18 |
+
size?: number;
|
| 19 |
+
};
|
| 20 |
+
};
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
// Basic health check
|
| 24 |
+
healthApp.get('/', async (c) => {
|
| 25 |
+
const startTime = Date.now();
|
| 26 |
+
const health: HealthStatus = {
|
| 27 |
+
status: 'healthy',
|
| 28 |
+
timestamp: new Date().toISOString(),
|
| 29 |
+
uptime: process.uptime(),
|
| 30 |
+
services: {
|
| 31 |
+
backend: { status: 'down' },
|
| 32 |
+
cache: { status: 'up' },
|
| 33 |
+
},
|
| 34 |
+
};
|
| 35 |
+
|
| 36 |
+
// Check backend health
|
| 37 |
+
try {
|
| 38 |
+
const backendStart = Date.now();
|
| 39 |
+
const response = await fetch(`${config.backendUrl}/health`, {
|
| 40 |
+
signal: AbortSignal.timeout(5000), // 5 second timeout
|
| 41 |
+
});
|
| 42 |
+
|
| 43 |
+
if (response.ok) {
|
| 44 |
+
health.services.backend = {
|
| 45 |
+
status: 'up',
|
| 46 |
+
responseTime: Date.now() - backendStart,
|
| 47 |
+
};
|
| 48 |
+
} else {
|
| 49 |
+
health.services.backend = {
|
| 50 |
+
status: 'down',
|
| 51 |
+
error: `HTTP ${response.status}`,
|
| 52 |
+
};
|
| 53 |
+
health.status = 'degraded';
|
| 54 |
+
}
|
| 55 |
+
} catch (error) {
|
| 56 |
+
health.services.backend = {
|
| 57 |
+
status: 'down',
|
| 58 |
+
error: error instanceof Error ? error.message : 'Unknown error',
|
| 59 |
+
};
|
| 60 |
+
health.status = 'degraded';
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
// Overall health determination
|
| 64 |
+
const allServicesUp = Object.values(health.services).every(s => s.status === 'up');
|
| 65 |
+
if (!allServicesUp) {
|
| 66 |
+
health.status = 'degraded';
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
// Return appropriate status code
|
| 70 |
+
const statusCode = health.status === 'healthy' ? 200 : 503;
|
| 71 |
+
|
| 72 |
+
return c.json(health, statusCode);
|
| 73 |
+
});
|
| 74 |
+
|
| 75 |
+
// Liveness probe (for k8s)
|
| 76 |
+
healthApp.get('/live', (c) => {
|
| 77 |
+
return c.json({ status: 'alive', timestamp: new Date().toISOString() });
|
| 78 |
+
});
|
| 79 |
+
|
| 80 |
+
// Readiness probe (for k8s)
|
| 81 |
+
healthApp.get('/ready', async (c) => {
|
| 82 |
+
try {
|
| 83 |
+
// Quick check if backend is reachable
|
| 84 |
+
const response = await fetch(`${config.backendUrl}/health`, {
|
| 85 |
+
signal: AbortSignal.timeout(2000),
|
| 86 |
+
});
|
| 87 |
+
|
| 88 |
+
if (response.ok) {
|
| 89 |
+
return c.json({ ready: true });
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
return c.json({ ready: false, reason: 'Backend not ready' }, 503);
|
| 93 |
+
} catch (error) {
|
| 94 |
+
return c.json({
|
| 95 |
+
ready: false,
|
| 96 |
+
reason: error instanceof Error ? error.message : 'Unknown error'
|
| 97 |
+
}, 503);
|
| 98 |
+
}
|
| 99 |
+
});
|
| 100 |
+
|
| 101 |
+
export { healthApp };
|
hono-proxy/src/routes/query-suggestions-vespa.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Hono } from 'hono';
|
| 2 |
+
import { cache } from '../services/cache';
|
| 3 |
+
|
| 4 |
+
const querySuggestionsApp = new Hono();
|
| 5 |
+
|
| 6 |
+
// Static suggestions for now (can be replaced with Vespa query later)
|
| 7 |
+
const staticSuggestions = [
|
| 8 |
+
'linqto bankruptcy',
|
| 9 |
+
'linqto filing date',
|
| 10 |
+
'linqto creditors',
|
| 11 |
+
'linqto assets',
|
| 12 |
+
'linqto liabilities',
|
| 13 |
+
'linqto chapter 11',
|
| 14 |
+
'linqto docket',
|
| 15 |
+
'linqto plan',
|
| 16 |
+
'linqto disclosure statement',
|
| 17 |
+
'linqto claims',
|
| 18 |
+
];
|
| 19 |
+
|
| 20 |
+
// Query suggestions endpoint
|
| 21 |
+
querySuggestionsApp.get('/', async (c) => {
|
| 22 |
+
try {
|
| 23 |
+
const query = c.req.query('query');
|
| 24 |
+
|
| 25 |
+
if (!query) {
|
| 26 |
+
return c.json({ suggestions: [] });
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
// Check cache
|
| 30 |
+
const cacheKey = `suggestions:${query}`;
|
| 31 |
+
const cachedSuggestions = cache.get(cacheKey);
|
| 32 |
+
|
| 33 |
+
if (cachedSuggestions) {
|
| 34 |
+
c.header('X-Cache', 'HIT');
|
| 35 |
+
return c.json(cachedSuggestions);
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
// Filter static suggestions based on query
|
| 39 |
+
const lowerQuery = query.toLowerCase();
|
| 40 |
+
const filteredSuggestions = staticSuggestions
|
| 41 |
+
.filter(s => s.toLowerCase().includes(lowerQuery))
|
| 42 |
+
.slice(0, 5);
|
| 43 |
+
|
| 44 |
+
const result = { suggestions: filteredSuggestions };
|
| 45 |
+
|
| 46 |
+
// Cache for 5 minutes
|
| 47 |
+
cache.set(cacheKey, result, 300);
|
| 48 |
+
c.header('X-Cache', 'MISS');
|
| 49 |
+
|
| 50 |
+
return c.json(result);
|
| 51 |
+
} catch (error) {
|
| 52 |
+
console.error('Suggestions error:', error);
|
| 53 |
+
return c.json({
|
| 54 |
+
error: 'Failed to fetch suggestions',
|
| 55 |
+
suggestions: []
|
| 56 |
+
}, 500);
|
| 57 |
+
}
|
| 58 |
+
});
|
| 59 |
+
|
| 60 |
+
export { querySuggestionsApp };
|
hono-proxy/src/routes/query-suggestions.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Hono } from 'hono';
|
| 2 |
+
import { config } from '../config';
|
| 3 |
+
import { cache } from '../services/cache';
|
| 4 |
+
|
| 5 |
+
const querySuggestionsApp = new Hono();
|
| 6 |
+
|
| 7 |
+
// Query suggestions endpoint - matches Next.js /api/query-suggestions
|
| 8 |
+
querySuggestionsApp.get('/', async (c) => {
|
| 9 |
+
try {
|
| 10 |
+
const query = c.req.query('query');
|
| 11 |
+
|
| 12 |
+
if (!query) {
|
| 13 |
+
return c.json({ suggestions: [] });
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
// Check cache
|
| 17 |
+
const cacheKey = `suggestions:${query}`;
|
| 18 |
+
const cachedSuggestions = cache.get(cacheKey);
|
| 19 |
+
|
| 20 |
+
if (cachedSuggestions) {
|
| 21 |
+
c.header('X-Cache', 'HIT');
|
| 22 |
+
return c.json(cachedSuggestions);
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
// Proxy to backend
|
| 26 |
+
const suggestionsUrl = `${config.backendUrl}/suggestions?query=${encodeURIComponent(query)}`;
|
| 27 |
+
const response = await fetch(suggestionsUrl);
|
| 28 |
+
|
| 29 |
+
if (!response.ok) {
|
| 30 |
+
throw new Error(`Backend returned ${response.status}`);
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
const data = await response.json();
|
| 34 |
+
|
| 35 |
+
// Cache for 5 minutes
|
| 36 |
+
cache.set(cacheKey, data, 300);
|
| 37 |
+
c.header('X-Cache', 'MISS');
|
| 38 |
+
|
| 39 |
+
return c.json(data);
|
| 40 |
+
} catch (error) {
|
| 41 |
+
console.error('Suggestions error:', error);
|
| 42 |
+
return c.json({
|
| 43 |
+
error: 'Failed to fetch suggestions',
|
| 44 |
+
suggestions: []
|
| 45 |
+
}, 500);
|
| 46 |
+
}
|
| 47 |
+
});
|
| 48 |
+
|
| 49 |
+
export { querySuggestionsApp };
|
hono-proxy/src/routes/search-direct.ts
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Hono } from 'hono';
|
| 2 |
+
import { z } from 'zod';
|
| 3 |
+
import { config } from '../config';
|
| 4 |
+
import { cache } from '../services/cache';
|
| 5 |
+
import { vespaRequest } from '../services/vespa-https';
|
| 6 |
+
import { v4 as uuidv4 } from 'uuid';
|
| 7 |
+
|
| 8 |
+
const searchApp = new Hono();
|
| 9 |
+
|
| 10 |
+
// Search request schema
|
| 11 |
+
const searchQuerySchema = z.object({
|
| 12 |
+
query: z.string().min(1).max(500),
|
| 13 |
+
ranking: z.enum(['hybrid', 'colpali', 'bm25']).optional().default('hybrid'),
|
| 14 |
+
});
|
| 15 |
+
|
| 16 |
+
// Main search endpoint - direct to Vespa
|
| 17 |
+
searchApp.get('/', async (c) => {
|
| 18 |
+
try {
|
| 19 |
+
const query = c.req.query('query');
|
| 20 |
+
const ranking = c.req.query('ranking') || 'hybrid';
|
| 21 |
+
|
| 22 |
+
const validation = searchQuerySchema.safeParse({ query, ranking });
|
| 23 |
+
|
| 24 |
+
if (!validation.success) {
|
| 25 |
+
return c.json({ error: 'Invalid request', details: validation.error.issues }, 400);
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
const validatedData = validation.data;
|
| 29 |
+
|
| 30 |
+
// Check cache
|
| 31 |
+
const cacheKey = `search:${validatedData.query}:${validatedData.ranking}`;
|
| 32 |
+
const cachedResult = cache.get(cacheKey);
|
| 33 |
+
|
| 34 |
+
if (cachedResult) {
|
| 35 |
+
c.header('X-Cache', 'HIT');
|
| 36 |
+
return c.json(cachedResult);
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
// Build YQL query based on ranking
|
| 40 |
+
let yql = '';
|
| 41 |
+
let rankProfile = 'default';
|
| 42 |
+
|
| 43 |
+
switch (validatedData.ranking) {
|
| 44 |
+
case 'colpali':
|
| 45 |
+
yql = `select * from linqto where userQuery() limit 20`;
|
| 46 |
+
rankProfile = 'colpali';
|
| 47 |
+
break;
|
| 48 |
+
case 'bm25':
|
| 49 |
+
yql = `select * from linqto where userQuery() order by bm25_score desc limit 20`;
|
| 50 |
+
break;
|
| 51 |
+
case 'hybrid':
|
| 52 |
+
default:
|
| 53 |
+
yql = `select * from linqto where userQuery() | rank (reciprocal_rank_fusion(bm25_score, max_sim)) limit 20`;
|
| 54 |
+
break;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
// Query Vespa directly
|
| 58 |
+
const searchUrl = `${config.vespaAppUrl}/search/`;
|
| 59 |
+
const searchParams = new URLSearchParams({
|
| 60 |
+
yql,
|
| 61 |
+
query: validatedData.query,
|
| 62 |
+
ranking: rankProfile,
|
| 63 |
+
hits: '20'
|
| 64 |
+
});
|
| 65 |
+
|
| 66 |
+
const response = await vespaRequest(`${searchUrl}?${searchParams}`);
|
| 67 |
+
|
| 68 |
+
if (!response.ok) {
|
| 69 |
+
const errorText = await response.text();
|
| 70 |
+
console.error('Vespa error:', errorText);
|
| 71 |
+
throw new Error(`Vespa returned ${response.status}: ${errorText}`);
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
const data = await response.json();
|
| 75 |
+
|
| 76 |
+
// Generate query_id for sim_map compatibility
|
| 77 |
+
const queryId = uuidv4();
|
| 78 |
+
|
| 79 |
+
// Transform to match expected format
|
| 80 |
+
if (data.root && data.root.children) {
|
| 81 |
+
data.root.children.forEach((hit: any, idx: number) => {
|
| 82 |
+
if (!hit.fields) hit.fields = {};
|
| 83 |
+
// Add sim_map identifier for compatibility
|
| 84 |
+
hit.fields.sim_map = `${queryId}_${idx}`;
|
| 85 |
+
});
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
// Cache the result
|
| 89 |
+
cache.set(cacheKey, data);
|
| 90 |
+
c.header('X-Cache', 'MISS');
|
| 91 |
+
|
| 92 |
+
return c.json(data);
|
| 93 |
+
} catch (error) {
|
| 94 |
+
console.error('Search error:', error);
|
| 95 |
+
return c.json({
|
| 96 |
+
error: 'Search failed',
|
| 97 |
+
message: error instanceof Error ? error.message : 'Unknown error'
|
| 98 |
+
}, 500);
|
| 99 |
+
}
|
| 100 |
+
});
|
| 101 |
+
|
| 102 |
+
// Full image endpoint
|
| 103 |
+
searchApp.get('/full-image', async (c) => {
|
| 104 |
+
try {
|
| 105 |
+
const docId = c.req.query('docId');
|
| 106 |
+
|
| 107 |
+
if (!docId) {
|
| 108 |
+
return c.json({ error: 'docId is required' }, 400);
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
// Check cache
|
| 112 |
+
const cacheKey = `fullimage:${docId}`;
|
| 113 |
+
const cachedImage = cache.get<{ base64_image: string }>(cacheKey);
|
| 114 |
+
|
| 115 |
+
if (cachedImage) {
|
| 116 |
+
c.header('X-Cache', 'HIT');
|
| 117 |
+
return c.json(cachedImage);
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
// Query Vespa for the document
|
| 121 |
+
const searchUrl = `${config.vespaAppUrl}/search/`;
|
| 122 |
+
const searchParams = new URLSearchParams({
|
| 123 |
+
yql: `select * from linqto where id contains "${docId}"`,
|
| 124 |
+
hits: '1'
|
| 125 |
+
});
|
| 126 |
+
|
| 127 |
+
const response = await vespaRequest(`${searchUrl}?${searchParams}`);
|
| 128 |
+
|
| 129 |
+
if (!response.ok) {
|
| 130 |
+
throw new Error(`Vespa returned ${response.status}`);
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
const data = await response.json();
|
| 134 |
+
|
| 135 |
+
if (data.root?.children?.[0]?.fields) {
|
| 136 |
+
const fields = data.root.children[0].fields;
|
| 137 |
+
const base64Image = fields.full_image || fields.image;
|
| 138 |
+
|
| 139 |
+
if (base64Image) {
|
| 140 |
+
const result = { base64_image: base64Image };
|
| 141 |
+
cache.set(cacheKey, result, 86400); // 24 hours
|
| 142 |
+
c.header('X-Cache', 'MISS');
|
| 143 |
+
return c.json(result);
|
| 144 |
+
}
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
return c.json({ error: 'Image not found' }, 404);
|
| 148 |
+
} catch (error) {
|
| 149 |
+
console.error('Full image error:', error);
|
| 150 |
+
return c.json({
|
| 151 |
+
error: 'Failed to fetch image',
|
| 152 |
+
message: error instanceof Error ? error.message : 'Unknown error'
|
| 153 |
+
}, 500);
|
| 154 |
+
}
|
| 155 |
+
});
|
| 156 |
+
|
| 157 |
+
// Query suggestions endpoint
|
| 158 |
+
searchApp.get('/suggestions', async (c) => {
|
| 159 |
+
try {
|
| 160 |
+
const query = c.req.query('query');
|
| 161 |
+
|
| 162 |
+
// Static suggestions for now
|
| 163 |
+
const staticSuggestions = [
|
| 164 |
+
'linqto bankruptcy',
|
| 165 |
+
'linqto filing date',
|
| 166 |
+
'linqto creditors',
|
| 167 |
+
'linqto assets',
|
| 168 |
+
'linqto liabilities',
|
| 169 |
+
'linqto chapter 11',
|
| 170 |
+
'linqto docket',
|
| 171 |
+
'linqto plan',
|
| 172 |
+
'linqto disclosure statement',
|
| 173 |
+
'linqto claims',
|
| 174 |
+
];
|
| 175 |
+
|
| 176 |
+
if (!query) {
|
| 177 |
+
return c.json({ suggestions: staticSuggestions.slice(0, 5) });
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
const lowerQuery = query.toLowerCase();
|
| 181 |
+
const filtered = staticSuggestions
|
| 182 |
+
.filter(s => s.toLowerCase().includes(lowerQuery))
|
| 183 |
+
.slice(0, 5);
|
| 184 |
+
|
| 185 |
+
return c.json({ suggestions: filtered });
|
| 186 |
+
} catch (error) {
|
| 187 |
+
console.error('Suggestions error:', error);
|
| 188 |
+
return c.json({
|
| 189 |
+
error: 'Failed to fetch suggestions',
|
| 190 |
+
suggestions: []
|
| 191 |
+
}, 500);
|
| 192 |
+
}
|
| 193 |
+
});
|
| 194 |
+
|
| 195 |
+
// Similarity maps endpoint (placeholder)
|
| 196 |
+
searchApp.get('/similarity-maps', async (c) => {
|
| 197 |
+
try {
|
| 198 |
+
const queryId = c.req.query('queryId');
|
| 199 |
+
const idx = c.req.query('idx');
|
| 200 |
+
const token = c.req.query('token');
|
| 201 |
+
const tokenIdx = c.req.query('tokenIdx');
|
| 202 |
+
|
| 203 |
+
if (!queryId || !idx || !token || !tokenIdx) {
|
| 204 |
+
return c.json({ error: 'Missing required parameters' }, 400);
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
// Return placeholder HTML
|
| 208 |
+
const html = `
|
| 209 |
+
<div style="padding: 20px; text-align: center;">
|
| 210 |
+
<h3>Similarity Map</h3>
|
| 211 |
+
<p>Query: ${token}</p>
|
| 212 |
+
<p>Document: ${idx}</p>
|
| 213 |
+
<p style="color: #666;">
|
| 214 |
+
Similarity map generation requires the ColPali model.
|
| 215 |
+
This is a placeholder for the demo.
|
| 216 |
+
</p>
|
| 217 |
+
</div>
|
| 218 |
+
`;
|
| 219 |
+
|
| 220 |
+
return c.html(html);
|
| 221 |
+
} catch (error) {
|
| 222 |
+
console.error('Similarity map error:', error);
|
| 223 |
+
return c.json({
|
| 224 |
+
error: 'Failed to generate similarity map',
|
| 225 |
+
message: error instanceof Error ? error.message : 'Unknown error'
|
| 226 |
+
}, 500);
|
| 227 |
+
}
|
| 228 |
+
});
|
| 229 |
+
|
| 230 |
+
export { searchApp };
|
hono-proxy/src/routes/search.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Hono } from 'hono';
|
| 2 |
+
import { z } from 'zod';
|
| 3 |
+
import { config } from '../config';
|
| 4 |
+
import { cache, cacheKeys } from '../services/cache';
|
| 5 |
+
|
| 6 |
+
const searchApp = new Hono();
|
| 7 |
+
|
| 8 |
+
// Search request schema for GET requests
|
| 9 |
+
const searchQuerySchema = z.object({
|
| 10 |
+
query: z.string().min(1).max(500),
|
| 11 |
+
ranking: z.enum(['hybrid', 'colpali', 'bm25']).optional().default('hybrid'),
|
| 12 |
+
});
|
| 13 |
+
|
| 14 |
+
// Main search endpoint - matches Next.js /api/colpali-search
|
| 15 |
+
searchApp.get('/', async (c) => {
|
| 16 |
+
try {
|
| 17 |
+
const query = c.req.query('query');
|
| 18 |
+
const ranking = c.req.query('ranking') || 'hybrid';
|
| 19 |
+
|
| 20 |
+
const validation = searchQuerySchema.safeParse({ query, ranking });
|
| 21 |
+
|
| 22 |
+
if (!validation.success) {
|
| 23 |
+
return c.json({ error: 'Invalid request', details: validation.error.issues }, 400);
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
const validatedData = validation.data;
|
| 27 |
+
|
| 28 |
+
// Check cache
|
| 29 |
+
const cacheKey = `search:${validatedData.query}:${validatedData.ranking}`;
|
| 30 |
+
const cachedResult = cache.get(cacheKey);
|
| 31 |
+
|
| 32 |
+
if (cachedResult) {
|
| 33 |
+
c.header('X-Cache', 'HIT');
|
| 34 |
+
return c.json(cachedResult);
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
// Proxy to backend /fetch_results endpoint
|
| 38 |
+
const searchUrl = `${config.backendUrl}/fetch_results?query=${encodeURIComponent(validatedData.query)}&ranking=${validatedData.ranking}`;
|
| 39 |
+
const response = await fetch(searchUrl);
|
| 40 |
+
|
| 41 |
+
if (!response.ok) {
|
| 42 |
+
throw new Error(`Backend returned ${response.status}`);
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
const data = await response.json();
|
| 46 |
+
|
| 47 |
+
// Cache the result
|
| 48 |
+
cache.set(cacheKey, data);
|
| 49 |
+
c.header('X-Cache', 'MISS');
|
| 50 |
+
|
| 51 |
+
return c.json(data);
|
| 52 |
+
} catch (error) {
|
| 53 |
+
console.error('Search error:', error);
|
| 54 |
+
return c.json({
|
| 55 |
+
error: 'Search failed',
|
| 56 |
+
message: error instanceof Error ? error.message : 'Unknown error'
|
| 57 |
+
}, 500);
|
| 58 |
+
}
|
| 59 |
+
});
|
| 60 |
+
|
| 61 |
+
// Full image endpoint - matches Next.js /api/full-image
|
| 62 |
+
searchApp.get('/full-image', async (c) => {
|
| 63 |
+
try {
|
| 64 |
+
const docId = c.req.query('docId');
|
| 65 |
+
|
| 66 |
+
if (!docId) {
|
| 67 |
+
return c.json({ error: 'docId is required' }, 400);
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
// Check cache
|
| 71 |
+
const cacheKey = `fullimage:${docId}`;
|
| 72 |
+
const cachedImage = cache.get<{ base64_image: string }>(cacheKey);
|
| 73 |
+
|
| 74 |
+
if (cachedImage) {
|
| 75 |
+
c.header('X-Cache', 'HIT');
|
| 76 |
+
return c.json(cachedImage);
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
// Proxy to backend
|
| 80 |
+
const imageUrl = `${config.backendUrl}/full_image?doc_id=${encodeURIComponent(docId)}`;
|
| 81 |
+
const response = await fetch(imageUrl);
|
| 82 |
+
|
| 83 |
+
if (!response.ok) {
|
| 84 |
+
throw new Error(`Backend returned ${response.status}`);
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
const data = await response.json();
|
| 88 |
+
|
| 89 |
+
// Cache for 24 hours
|
| 90 |
+
cache.set(cacheKey, data, 86400);
|
| 91 |
+
c.header('X-Cache', 'MISS');
|
| 92 |
+
|
| 93 |
+
return c.json(data);
|
| 94 |
+
} catch (error) {
|
| 95 |
+
console.error('Full image error:', error);
|
| 96 |
+
return c.json({
|
| 97 |
+
error: 'Failed to fetch image',
|
| 98 |
+
message: error instanceof Error ? error.message : 'Unknown error'
|
| 99 |
+
}, 500);
|
| 100 |
+
}
|
| 101 |
+
});
|
| 102 |
+
|
| 103 |
+
// Query suggestions endpoint - matches Next.js /api/query-suggestions
|
| 104 |
+
searchApp.get('/suggestions', async (c) => {
|
| 105 |
+
try {
|
| 106 |
+
const query = c.req.query('query');
|
| 107 |
+
|
| 108 |
+
if (!query) {
|
| 109 |
+
return c.json({ suggestions: [] });
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
// Check cache
|
| 113 |
+
const cacheKey = `suggestions:${query}`;
|
| 114 |
+
const cachedSuggestions = cache.get(cacheKey);
|
| 115 |
+
|
| 116 |
+
if (cachedSuggestions) {
|
| 117 |
+
c.header('X-Cache', 'HIT');
|
| 118 |
+
return c.json(cachedSuggestions);
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
// Proxy to backend
|
| 122 |
+
const suggestionsUrl = `${config.backendUrl}/suggestions?query=${encodeURIComponent(query)}`;
|
| 123 |
+
const response = await fetch(suggestionsUrl);
|
| 124 |
+
|
| 125 |
+
if (!response.ok) {
|
| 126 |
+
throw new Error(`Backend returned ${response.status}`);
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
const data = await response.json();
|
| 130 |
+
|
| 131 |
+
// Cache for 5 minutes
|
| 132 |
+
cache.set(cacheKey, data, 300);
|
| 133 |
+
c.header('X-Cache', 'MISS');
|
| 134 |
+
|
| 135 |
+
return c.json(data);
|
| 136 |
+
} catch (error) {
|
| 137 |
+
console.error('Suggestions error:', error);
|
| 138 |
+
return c.json({
|
| 139 |
+
error: 'Failed to fetch suggestions',
|
| 140 |
+
suggestions: []
|
| 141 |
+
}, 500);
|
| 142 |
+
}
|
| 143 |
+
});
|
| 144 |
+
|
| 145 |
+
// Similarity maps endpoint - matches Next.js /api/similarity-maps
|
| 146 |
+
searchApp.get('/similarity-maps', async (c) => {
|
| 147 |
+
try {
|
| 148 |
+
const queryId = c.req.query('queryId');
|
| 149 |
+
const idx = c.req.query('idx');
|
| 150 |
+
const token = c.req.query('token');
|
| 151 |
+
const tokenIdx = c.req.query('tokenIdx');
|
| 152 |
+
|
| 153 |
+
if (!queryId || !idx || !token || !tokenIdx) {
|
| 154 |
+
return c.json({ error: 'Missing required parameters' }, 400);
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
// Note: Similarity maps are dynamic, so no caching
|
| 158 |
+
const simMapUrl = `${config.backendUrl}/get_sim_map?query_id=${encodeURIComponent(queryId)}&idx=${idx}&token=${encodeURIComponent(token)}&token_idx=${tokenIdx}`;
|
| 159 |
+
const response = await fetch(simMapUrl);
|
| 160 |
+
|
| 161 |
+
if (!response.ok) {
|
| 162 |
+
throw new Error(`Backend returned ${response.status}`);
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
// Backend returns HTML, so we need to return it as text
|
| 166 |
+
const html = await response.text();
|
| 167 |
+
|
| 168 |
+
return c.html(html);
|
| 169 |
+
} catch (error) {
|
| 170 |
+
console.error('Similarity map error:', error);
|
| 171 |
+
return c.json({
|
| 172 |
+
error: 'Failed to generate similarity map',
|
| 173 |
+
message: error instanceof Error ? error.message : 'Unknown error'
|
| 174 |
+
}, 500);
|
| 175 |
+
}
|
| 176 |
+
});
|
| 177 |
+
|
| 178 |
+
export { searchApp };
|
hono-proxy/src/routes/similarity-maps.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Hono } from 'hono';
|
| 2 |
+
import { config } from '../config';
|
| 3 |
+
|
| 4 |
+
const similarityMapsApp = new Hono();
|
| 5 |
+
|
| 6 |
+
// Similarity maps endpoint - matches Next.js /api/similarity-maps
|
| 7 |
+
similarityMapsApp.get('/', async (c) => {
|
| 8 |
+
try {
|
| 9 |
+
const queryId = c.req.query('queryId');
|
| 10 |
+
const idx = c.req.query('idx');
|
| 11 |
+
const token = c.req.query('token');
|
| 12 |
+
const tokenIdx = c.req.query('tokenIdx');
|
| 13 |
+
|
| 14 |
+
if (!queryId || !idx || !token || !tokenIdx) {
|
| 15 |
+
return c.json({ error: 'Missing required parameters' }, 400);
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
// Note: Similarity maps are dynamic, so no caching
|
| 19 |
+
const simMapUrl = `${config.backendUrl}/get_sim_map?query_id=${encodeURIComponent(queryId)}&idx=${idx}&token=${encodeURIComponent(token)}&token_idx=${tokenIdx}`;
|
| 20 |
+
const response = await fetch(simMapUrl);
|
| 21 |
+
|
| 22 |
+
if (!response.ok) {
|
| 23 |
+
throw new Error(`Backend returned ${response.status}`);
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
// Backend returns HTML, so we need to return it as text
|
| 27 |
+
const html = await response.text();
|
| 28 |
+
|
| 29 |
+
return c.html(html);
|
| 30 |
+
} catch (error) {
|
| 31 |
+
console.error('Similarity map error:', error);
|
| 32 |
+
return c.json({
|
| 33 |
+
error: 'Failed to generate similarity map',
|
| 34 |
+
message: error instanceof Error ? error.message : 'Unknown error'
|
| 35 |
+
}, 500);
|
| 36 |
+
}
|
| 37 |
+
});
|
| 38 |
+
|
| 39 |
+
export { similarityMapsApp };
|
hono-proxy/src/routes/visual-rag-chat.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Hono } from 'hono';
|
| 2 |
+
import { streamSSE } from 'hono/streaming';
|
| 3 |
+
import { config } from '../config';
|
| 4 |
+
|
| 5 |
+
const visualRagChatApp = new Hono();
|
| 6 |
+
|
| 7 |
+
// Visual RAG Chat SSE endpoint - matches Next.js /api/visual-rag-chat
|
| 8 |
+
visualRagChatApp.get('/', async (c) => {
|
| 9 |
+
const queryId = c.req.query('queryId');
|
| 10 |
+
const query = c.req.query('query');
|
| 11 |
+
const docIds = c.req.query('docIds');
|
| 12 |
+
|
| 13 |
+
if (!queryId || !query || !docIds) {
|
| 14 |
+
return c.json({ error: 'Missing required parameters: queryId, query, docIds' }, 400);
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
return streamSSE(c, async (stream) => {
|
| 18 |
+
try {
|
| 19 |
+
// Create abort controller for cleanup
|
| 20 |
+
const abortController = new AbortController();
|
| 21 |
+
|
| 22 |
+
// Forward request to backend /get-message endpoint
|
| 23 |
+
const chatUrl = `${config.backendUrl}/get-message?query_id=${encodeURIComponent(queryId)}&query=${encodeURIComponent(query)}&doc_ids=${encodeURIComponent(docIds)}`;
|
| 24 |
+
|
| 25 |
+
const response = await fetch(chatUrl, {
|
| 26 |
+
headers: {
|
| 27 |
+
'Accept': 'text/event-stream',
|
| 28 |
+
},
|
| 29 |
+
signal: abortController.signal,
|
| 30 |
+
});
|
| 31 |
+
|
| 32 |
+
if (!response.ok) {
|
| 33 |
+
await stream.writeSSE({
|
| 34 |
+
event: 'error',
|
| 35 |
+
data: JSON.stringify({ error: `Backend returned ${response.status}` }),
|
| 36 |
+
});
|
| 37 |
+
return;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
if (!response.body) {
|
| 41 |
+
await stream.writeSSE({
|
| 42 |
+
event: 'error',
|
| 43 |
+
data: JSON.stringify({ error: 'No response body' }),
|
| 44 |
+
});
|
| 45 |
+
return;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
// Stream the response
|
| 49 |
+
const reader = response.body.getReader();
|
| 50 |
+
const decoder = new TextDecoder();
|
| 51 |
+
let buffer = '';
|
| 52 |
+
|
| 53 |
+
while (true) {
|
| 54 |
+
const { done, value } = await reader.read();
|
| 55 |
+
|
| 56 |
+
if (done) break;
|
| 57 |
+
|
| 58 |
+
buffer += decoder.decode(value, { stream: true });
|
| 59 |
+
const lines = buffer.split('\n');
|
| 60 |
+
|
| 61 |
+
// Keep the last incomplete line in the buffer
|
| 62 |
+
buffer = lines.pop() || '';
|
| 63 |
+
|
| 64 |
+
for (const line of lines) {
|
| 65 |
+
if (line.trim() === '') continue;
|
| 66 |
+
|
| 67 |
+
if (line.startsWith('data: ')) {
|
| 68 |
+
const data = line.slice(6);
|
| 69 |
+
await stream.writeSSE({ data });
|
| 70 |
+
} else if (line.startsWith('event: ')) {
|
| 71 |
+
// Handle event lines if backend sends them
|
| 72 |
+
const event = line.slice(7).trim();
|
| 73 |
+
// Look for the next data line
|
| 74 |
+
const nextLineIndex = lines.indexOf(line) + 1;
|
| 75 |
+
if (nextLineIndex < lines.length) {
|
| 76 |
+
const nextLine = lines[nextLineIndex];
|
| 77 |
+
if (nextLine.startsWith('data: ')) {
|
| 78 |
+
const data = nextLine.slice(6);
|
| 79 |
+
await stream.writeSSE({ event, data });
|
| 80 |
+
lines.splice(nextLineIndex, 1); // Remove processed line
|
| 81 |
+
}
|
| 82 |
+
}
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
// Handle any remaining data in buffer
|
| 88 |
+
if (buffer.trim()) {
|
| 89 |
+
if (buffer.startsWith('data: ')) {
|
| 90 |
+
await stream.writeSSE({ data: buffer.slice(6) });
|
| 91 |
+
}
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
// Cleanup
|
| 95 |
+
abortController.abort();
|
| 96 |
+
} catch (error) {
|
| 97 |
+
console.error('Chat streaming error:', error);
|
| 98 |
+
await stream.writeSSE({
|
| 99 |
+
event: 'error',
|
| 100 |
+
data: JSON.stringify({
|
| 101 |
+
error: 'Streaming failed',
|
| 102 |
+
message: error instanceof Error ? error.message : 'Unknown error'
|
| 103 |
+
}),
|
| 104 |
+
});
|
| 105 |
+
}
|
| 106 |
+
});
|
| 107 |
+
});
|
| 108 |
+
|
| 109 |
+
export { visualRagChatApp };
|
hono-proxy/src/services/cache.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { config } from '../config';
|
| 2 |
+
|
| 3 |
+
interface CacheEntry<T> {
|
| 4 |
+
data: T;
|
| 5 |
+
expiry: number;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
class InMemoryCache {
|
| 9 |
+
private cache: Map<string, CacheEntry<any>> = new Map();
|
| 10 |
+
private cleanupInterval: NodeJS.Timeout;
|
| 11 |
+
|
| 12 |
+
constructor() {
|
| 13 |
+
// Cleanup expired entries every minute
|
| 14 |
+
this.cleanupInterval = setInterval(() => this.cleanup(), 60000);
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
set<T>(key: string, value: T, ttl: number = config.cacheTTL): void {
|
| 18 |
+
if (!config.enableCache) return;
|
| 19 |
+
|
| 20 |
+
const expiry = Date.now() + (ttl * 1000);
|
| 21 |
+
this.cache.set(key, { data: value, expiry });
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
get<T>(key: string): T | null {
|
| 25 |
+
if (!config.enableCache) return null;
|
| 26 |
+
|
| 27 |
+
const entry = this.cache.get(key);
|
| 28 |
+
if (!entry) return null;
|
| 29 |
+
|
| 30 |
+
if (Date.now() > entry.expiry) {
|
| 31 |
+
this.cache.delete(key);
|
| 32 |
+
return null;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
return entry.data as T;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
delete(key: string): void {
|
| 39 |
+
this.cache.delete(key);
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
clear(): void {
|
| 43 |
+
this.cache.clear();
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
private cleanup(): void {
|
| 47 |
+
const now = Date.now();
|
| 48 |
+
for (const [key, entry] of this.cache.entries()) {
|
| 49 |
+
if (now > entry.expiry) {
|
| 50 |
+
this.cache.delete(key);
|
| 51 |
+
}
|
| 52 |
+
}
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
destroy(): void {
|
| 56 |
+
clearInterval(this.cleanupInterval);
|
| 57 |
+
this.cache.clear();
|
| 58 |
+
}
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
export const cache = new InMemoryCache();
|
| 62 |
+
|
| 63 |
+
// Cache key generators
|
| 64 |
+
export const cacheKeys = {
|
| 65 |
+
search: (query: string, limit: number) => `search:${query}:${limit}`,
|
| 66 |
+
image: (docId: string, type: 'thumbnail' | 'full') => `image:${docId}:${type}`,
|
| 67 |
+
similarityMap: (docId: string, query: string) => `similarity:${docId}:${query}`,
|
| 68 |
+
};
|
hono-proxy/src/services/vespa-client-simple.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { config } from '../config';
|
| 2 |
+
|
| 3 |
+
// For now, we'll use regular fetch without certificate support
|
| 4 |
+
// This requires Vespa to be configured with token authentication
|
| 5 |
+
// or to have a proxy that handles certificates
|
| 6 |
+
|
| 7 |
+
export async function vespaFetch(url: string, options: RequestInit = {}) {
|
| 8 |
+
// Since browser fetch doesn't support client certificates,
|
| 9 |
+
// we'll need to either:
|
| 10 |
+
// 1. Use token authentication (if configured in Vespa)
|
| 11 |
+
// 2. Set up a proxy that handles certificates
|
| 12 |
+
// 3. Use the Python backend as a proxy
|
| 13 |
+
|
| 14 |
+
// For now, we'll attempt direct connection
|
| 15 |
+
// This will work if Vespa is configured for public access or token auth
|
| 16 |
+
return fetch(url, {
|
| 17 |
+
...options,
|
| 18 |
+
headers: {
|
| 19 |
+
...options.headers,
|
| 20 |
+
'Accept': 'application/json',
|
| 21 |
+
}
|
| 22 |
+
});
|
| 23 |
+
}
|
hono-proxy/src/services/vespa-client.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as fs from 'fs';
|
| 2 |
+
import * as https from 'https';
|
| 3 |
+
import { config } from '../config';
|
| 4 |
+
|
| 5 |
+
// Create HTTPS agent with certificate authentication
|
| 6 |
+
let httpsAgent: https.Agent | undefined;
|
| 7 |
+
|
| 8 |
+
if (config.vespaCertPath && config.vespaKeyPath) {
|
| 9 |
+
try {
|
| 10 |
+
httpsAgent = new https.Agent({
|
| 11 |
+
cert: fs.readFileSync(config.vespaCertPath),
|
| 12 |
+
key: fs.readFileSync(config.vespaKeyPath),
|
| 13 |
+
rejectUnauthorized: false
|
| 14 |
+
});
|
| 15 |
+
} catch (error) {
|
| 16 |
+
console.error('Failed to load Vespa certificates:', error);
|
| 17 |
+
}
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
export async function vespaFetch(url: string, options: RequestInit = {}) {
|
| 21 |
+
// For Node.js 18+, we need to use undici or node-fetch with agent support
|
| 22 |
+
const fetch = globalThis.fetch;
|
| 23 |
+
|
| 24 |
+
if (httpsAgent) {
|
| 25 |
+
// @ts-ignore - agent is not in standard fetch types but works in Node.js
|
| 26 |
+
return fetch(url, {
|
| 27 |
+
...options,
|
| 28 |
+
agent: httpsAgent
|
| 29 |
+
});
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
return fetch(url, options);
|
| 33 |
+
}
|
hono-proxy/src/services/vespa-https.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as https from 'https';
|
| 2 |
+
import * as fs from 'fs';
|
| 3 |
+
import { config } from '../config';
|
| 4 |
+
|
| 5 |
+
interface VespaRequestOptions {
|
| 6 |
+
method?: string;
|
| 7 |
+
headers?: Record<string, string>;
|
| 8 |
+
body?: string;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
export async function vespaRequest(url: string, options: VespaRequestOptions = {}): Promise<any> {
|
| 12 |
+
return new Promise((resolve, reject) => {
|
| 13 |
+
const urlObj = new URL(url);
|
| 14 |
+
|
| 15 |
+
const httpsOptions: https.RequestOptions = {
|
| 16 |
+
hostname: urlObj.hostname,
|
| 17 |
+
port: 443,
|
| 18 |
+
path: urlObj.pathname + urlObj.search,
|
| 19 |
+
method: options.method || 'GET',
|
| 20 |
+
headers: {
|
| 21 |
+
'Accept': 'application/json',
|
| 22 |
+
'Content-Type': 'application/json',
|
| 23 |
+
...options.headers
|
| 24 |
+
}
|
| 25 |
+
};
|
| 26 |
+
|
| 27 |
+
// Add certificate authentication if available
|
| 28 |
+
if (config.vespaCertPath && config.vespaKeyPath) {
|
| 29 |
+
try {
|
| 30 |
+
httpsOptions.cert = fs.readFileSync(config.vespaCertPath);
|
| 31 |
+
httpsOptions.key = fs.readFileSync(config.vespaKeyPath);
|
| 32 |
+
httpsOptions.rejectUnauthorized = false;
|
| 33 |
+
} catch (error) {
|
| 34 |
+
console.error('Failed to load certificates:', error);
|
| 35 |
+
}
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
const req = https.request(httpsOptions, (res) => {
|
| 39 |
+
let data = '';
|
| 40 |
+
|
| 41 |
+
res.on('data', (chunk) => {
|
| 42 |
+
data += chunk;
|
| 43 |
+
});
|
| 44 |
+
|
| 45 |
+
res.on('end', () => {
|
| 46 |
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
| 47 |
+
try {
|
| 48 |
+
resolve({
|
| 49 |
+
ok: true,
|
| 50 |
+
status: res.statusCode,
|
| 51 |
+
json: async () => JSON.parse(data),
|
| 52 |
+
text: async () => data
|
| 53 |
+
});
|
| 54 |
+
} catch (error) {
|
| 55 |
+
reject(error);
|
| 56 |
+
}
|
| 57 |
+
} else if (res.statusCode === 504) {
|
| 58 |
+
// Handle timeout as success if we got data
|
| 59 |
+
try {
|
| 60 |
+
const parsed = JSON.parse(data);
|
| 61 |
+
if (parsed.root && parsed.root.children) {
|
| 62 |
+
resolve({
|
| 63 |
+
ok: false, // Keep ok: false for proper handling
|
| 64 |
+
status: res.statusCode,
|
| 65 |
+
json: async () => parsed,
|
| 66 |
+
text: async () => data
|
| 67 |
+
});
|
| 68 |
+
} else {
|
| 69 |
+
resolve({
|
| 70 |
+
ok: false,
|
| 71 |
+
status: res.statusCode,
|
| 72 |
+
text: async () => data
|
| 73 |
+
});
|
| 74 |
+
}
|
| 75 |
+
} catch (error) {
|
| 76 |
+
resolve({
|
| 77 |
+
ok: false,
|
| 78 |
+
status: res.statusCode,
|
| 79 |
+
text: async () => data
|
| 80 |
+
});
|
| 81 |
+
}
|
| 82 |
+
} else {
|
| 83 |
+
resolve({
|
| 84 |
+
ok: false,
|
| 85 |
+
status: res.statusCode,
|
| 86 |
+
text: async () => data
|
| 87 |
+
});
|
| 88 |
+
}
|
| 89 |
+
});
|
| 90 |
+
});
|
| 91 |
+
|
| 92 |
+
req.on('error', (error) => {
|
| 93 |
+
reject(error);
|
| 94 |
+
});
|
| 95 |
+
|
| 96 |
+
if (options.body) {
|
| 97 |
+
req.write(options.body);
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
req.end();
|
| 101 |
+
});
|
| 102 |
+
}
|
hono-proxy/start.sh
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
# ColPali Hono Proxy Quick Start Script
|
| 4 |
+
|
| 5 |
+
echo "π ColPali Hono Proxy Setup"
|
| 6 |
+
echo "=========================="
|
| 7 |
+
|
| 8 |
+
# Check if .env exists
|
| 9 |
+
if [ ! -f .env ]; then
|
| 10 |
+
echo "π Creating .env file from template..."
|
| 11 |
+
cp .env.example .env
|
| 12 |
+
echo "β οΈ Please update .env with your configuration"
|
| 13 |
+
echo ""
|
| 14 |
+
fi
|
| 15 |
+
|
| 16 |
+
# Install dependencies if needed
|
| 17 |
+
if [ ! -d "node_modules" ]; then
|
| 18 |
+
echo "π¦ Installing dependencies..."
|
| 19 |
+
npm install
|
| 20 |
+
echo ""
|
| 21 |
+
fi
|
| 22 |
+
|
| 23 |
+
# Check if backend is running
|
| 24 |
+
echo "π Checking backend connection..."
|
| 25 |
+
BACKEND_URL=${BACKEND_URL:-http://localhost:7860}
|
| 26 |
+
if curl -f -s "$BACKEND_URL/health" > /dev/null; then
|
| 27 |
+
echo "β
Backend is reachable at $BACKEND_URL"
|
| 28 |
+
else
|
| 29 |
+
echo "β οΈ Warning: Backend at $BACKEND_URL is not responding"
|
| 30 |
+
echo " Make sure your ColPali backend is running"
|
| 31 |
+
fi
|
| 32 |
+
echo ""
|
| 33 |
+
|
| 34 |
+
# Start the server
|
| 35 |
+
echo "π Starting Hono proxy server..."
|
| 36 |
+
echo " API URL: http://localhost:4000/api"
|
| 37 |
+
echo " Health: http://localhost:4000/health"
|
| 38 |
+
echo ""
|
| 39 |
+
|
| 40 |
+
npm run dev
|
hono-proxy/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "ES2022",
|
| 4 |
+
"module": "commonjs",
|
| 5 |
+
"lib": ["ES2022"],
|
| 6 |
+
"outDir": "./dist",
|
| 7 |
+
"rootDir": "./src",
|
| 8 |
+
"strict": true,
|
| 9 |
+
"esModuleInterop": true,
|
| 10 |
+
"skipLibCheck": true,
|
| 11 |
+
"forceConsistentCasingInFileNames": true,
|
| 12 |
+
"resolveJsonModule": true,
|
| 13 |
+
"moduleResolution": "node",
|
| 14 |
+
"types": ["node"]
|
| 15 |
+
},
|
| 16 |
+
"include": ["src/**/*"],
|
| 17 |
+
"exclude": ["node_modules", "dist"]
|
| 18 |
+
}
|
requirements_embedding.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.109.0
|
| 2 |
+
uvicorn==0.25.0
|
| 3 |
+
torch>=2.0.0
|
| 4 |
+
torchvision
|
| 5 |
+
transformers>=4.36.0
|
| 6 |
+
colpali-engine>=0.2.0
|
| 7 |
+
numpy
|
| 8 |
+
Pillow
|
| 9 |
+
python-multipart
|
vespa-certs/data-plane-private-key.pem
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-----BEGIN PRIVATE KEY-----
|
| 2 |
+
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPilyxGAC2u3U8UJt
|
| 3 |
+
/ge1POIYBISa6kK5wkREPFEQBEWhRANCAARU7WOc2KNJIVKVZi+Q/yhB56gRedqe
|
| 4 |
+
X31rKMcTiV3i6ub/JZ2Vb0Uu3Uh5z8pR+8BDsDA2Z/kegHZ/SCNumdc9
|
| 5 |
+
-----END PRIVATE KEY-----
|
vespa-certs/data-plane-public-cert.pem
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-----BEGIN CERTIFICATE-----
|
| 2 |
+
MIIBOTCB36ADAgECAhEAw/MfxwQkH780EYUSADpR/zAKBggqhkjOPQQDAjAeMRww
|
| 3 |
+
GgYDVQQDExNjbG91ZC52ZXNwYS5leGFtcGxlMB4XDTI1MDcyMzA5NTA0OVoXDTM1
|
| 4 |
+
MDcyMTA5NTA0OVowHjEcMBoGA1UEAxMTY2xvdWQudmVzcGEuZXhhbXBsZTBZMBMG
|
| 5 |
+
ByqGSM49AgEGCCqGSM49AwEHA0IABFTtY5zYo0khUpVmL5D/KEHnqBF52p5ffWso
|
| 6 |
+
xxOJXeLq5v8lnZVvRS7dSHnPylH7wEOwMDZn+R6Adn9II26Z1z0wCgYIKoZIzj0E
|
| 7 |
+
AwIDSQAwRgIhANn7YhE5UkGItamxHas6lJjhhKoWIhSIsUMEmaXuiIZZAiEAvBEQ
|
| 8 |
+
YHCIi5v6LeeOwD0bkkVP/Rkny7q/4oc9ag3lU/0=
|
| 9 |
+
-----END CERTIFICATE-----
|