Spaces:
Running
Running
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-----
|