vk98 commited on
Commit
5dfbe50
Β·
0 Parent(s):

Initial backend deployment - Hono proxy + ColPali embedding API

Browse files
Files changed (46) hide show
  1. .env.example +9 -0
  2. .gitignore +5 -0
  3. Dockerfile +70 -0
  4. README.md +47 -0
  5. embedding_api.py +166 -0
  6. hono-proxy/.env.backend-hf +23 -0
  7. hono-proxy/.env.example +22 -0
  8. hono-proxy/.env.hf +22 -0
  9. hono-proxy/.gitignore +13 -0
  10. hono-proxy/Dockerfile +56 -0
  11. hono-proxy/README-NEXTJS-COMPATIBILITY.md +127 -0
  12. hono-proxy/README.md +207 -0
  13. hono-proxy/client-example.ts +156 -0
  14. hono-proxy/colpali-response.json +1 -0
  15. hono-proxy/docker-compose.yml +29 -0
  16. hono-proxy/ecosystem.config.js +23 -0
  17. hono-proxy/package-lock.json +751 -0
  18. hono-proxy/package.json +26 -0
  19. hono-proxy/src/config/index.ts +44 -0
  20. hono-proxy/src/index.ts +106 -0
  21. hono-proxy/src/middleware/cors.ts +18 -0
  22. hono-proxy/src/middleware/logger.ts +13 -0
  23. hono-proxy/src/middleware/rateLimit.ts +54 -0
  24. hono-proxy/src/routes/api.ts +274 -0
  25. hono-proxy/src/routes/backend-api.ts +376 -0
  26. hono-proxy/src/routes/chat-direct.ts +46 -0
  27. hono-proxy/src/routes/chat.ts +109 -0
  28. hono-proxy/src/routes/colpali-search-vespa.ts +107 -0
  29. hono-proxy/src/routes/colpali-search.ts +61 -0
  30. hono-proxy/src/routes/full-image.ts +49 -0
  31. hono-proxy/src/routes/health.ts +101 -0
  32. hono-proxy/src/routes/query-suggestions-vespa.ts +60 -0
  33. hono-proxy/src/routes/query-suggestions.ts +49 -0
  34. hono-proxy/src/routes/search-direct.ts +230 -0
  35. hono-proxy/src/routes/search.ts +178 -0
  36. hono-proxy/src/routes/similarity-maps.ts +39 -0
  37. hono-proxy/src/routes/visual-rag-chat.ts +109 -0
  38. hono-proxy/src/services/cache.ts +68 -0
  39. hono-proxy/src/services/vespa-client-simple.ts +23 -0
  40. hono-proxy/src/services/vespa-client.ts +33 -0
  41. hono-proxy/src/services/vespa-https.ts +102 -0
  42. hono-proxy/start.sh +40 -0
  43. hono-proxy/tsconfig.json +18 -0
  44. requirements_embedding.txt +9 -0
  45. vespa-certs/data-plane-private-key.pem +5 -0
  46. 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-----