diff --git a/.github/workflows/build-and-deploy-fallback.yml b/.github/workflows/build-and-deploy-fallback.yml new file mode 100644 index 0000000..eaa92e4 --- /dev/null +++ b/.github/workflows/build-and-deploy-fallback.yml @@ -0,0 +1,101 @@ +# .github/workflows/build-and-deploy.yml + +name: Build and Deploy Fallback + +on: + push: + branches: ["gemini"] + +jobs: + build-and-deploy: + permissions: + contents: read + packages: write + + name: Build Images and Deploy to Server + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set repo name to lowercase + id: repo_name + run: echo "name=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Create web-app .env file + run: echo 'GEMINI_API_KEY=${{ secrets.GEMINI_API_KEY }}' > web-app/.env + + - name: Build and push web-app image πŸš€ + uses: docker/build-push-action@v6 + with: + context: ./web-app + push: true + tags: ghcr.io/${{ steps.repo_name.outputs.name }}/web-app:${{ github.sha }} + cache-from: type=gha,scope=web-app + cache-to: type=gha,mode=max,scope=web-app + + - name: Ensure remote deploy directory exists + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.SERVER_HOST }} + username: ${{ secrets.SERVER_USERNAME }} + key: ${{ secrets.SSH_PRIVATE_KEY }} + script: | + mkdir -p /home/github-actions/codered-astra + + - name: Upload compose files to server + uses: appleboy/scp-action@v0.1.7 + with: + host: ${{ secrets.SERVER_HOST }} + username: ${{ secrets.SERVER_USERNAME }} + key: ${{ secrets.SSH_PRIVATE_KEY }} + source: "docker-compose.yml,docker-compose.prod.yml" + target: "/home/github-actions/codered-astra/" + + - name: Deploy to server via SSH ☁️ + uses: appleboy/ssh-action@v1.0.3 + env: + RUNNER_GH_ACTOR: ${{ github.actor }} + RUNNER_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + host: ${{ secrets.SERVER_HOST }} + username: ${{ secrets.SERVER_USERNAME }} + key: ${{ secrets.SSH_PRIVATE_KEY }} + # pass selected env vars to the remote shell so docker login works + envs: RUNNER_GITHUB_TOKEN,RUNNER_GH_ACTOR + debug: true + script: | + cd /home/github-actions/codered-astra + chmod -R o+rX rust-engine/demo-data + # wrapper to support both Docker Compose v2 and legacy v1 + compose() { docker compose "$@" || docker-compose "$@"; } + # Log in to GHCR using the run's GITHUB_TOKEN so compose can pull images. + if [ -n "$RUNNER_GITHUB_TOKEN" ] && [ -n "$RUNNER_GH_ACTOR" ]; then + echo "$RUNNER_GITHUB_TOKEN" | docker login ghcr.io -u "$RUNNER_GH_ACTOR" --password-stdin || true + fi + export REPO_NAME_LOWER='${{ steps.repo_name.outputs.name }}' + export GEMINI_API_KEY='${{ secrets.GEMINI_API_KEY }}' + export MYSQL_DATABASE='${{ secrets.MYSQL_DATABASE }}' + export MYSQL_USER='${{ secrets.MYSQL_USER }}' + export MYSQL_PASSWORD='${{ secrets.MYSQL_PASSWORD }}' + export MYSQL_ROOT_PASSWORD='${{ secrets.MYSQL_ROOT_PASSWORD }}' + export IMAGE_TAG=${{ github.sha }} + # Stop and remove old containers before pulling new images + compose -f docker-compose.prod.yml down + # Clear previous logs for a clean deployment log + : > ~/astra-logs/astra-errors.log || true + compose -f docker-compose.prod.yml pull + compose -f docker-compose.prod.yml up -d + # Security hygiene: remove GHCR credentials after pulling + docker logout ghcr.io || true \ No newline at end of file diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..b61318a --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,268 @@ +# CodeRED-Astra Architecture + +## Overview + +CodeRED-Astra is a Retrieval-Augmented Generation (RAG) system for querying ISS technical documentation using vector search, MySQL metadata storage, and Gemini AI for analysis and response generation. + +## System Components + +### 1. **Rust Backend** (`rust-engine/`) +High-performance Rust backend using Warp for HTTP, SQLx for MySQL, and Reqwest for external API calls. + +#### Modules + +**`main.rs`** - Entry point +- Initializes tracing, database, storage +- Spawns FileWorker and QueryWorker background tasks +- Serves API routes on port 8000 + +**`db.rs`** - Database initialization +- Connects to MySQL +- Creates `files` table (id, filename, path, description, pending_analysis, analysis_status) +- Creates `queries` table (id, status, payload, result, timestamps) + +**`api.rs`** - HTTP endpoints +- `POST /api/files` - Upload file (multipart/form-data) +- `POST /api/files/import-demo` - Bulk import from demo-data directory +- `GET /api/files/list` - List all files with status +- `GET /api/files/delete?id=` - Delete file and remove from Qdrant +- `POST /api/query/create` - Create new query (returns query ID) +- `GET /api/query/status?id=` - Check query status +- `GET /api/query/result?id=` - Get query result +- `GET /api/query/cancel?id=` - Cancel in-progress query + +**`file_worker.rs`** - File analysis pipeline +- **Background worker** that processes files with `pending_analysis = TRUE` +- Claims stale/queued files (requeues if stuck >10 min) +- **Stage 1**: Call Gemini 1.5 Flash for initial description +- **Stage 2**: Call Gemini 1.5 Pro for deep vector graph data (keywords, relationships) +- **Stage 3**: Generate embedding and upsert to Qdrant +- **Stage 4**: Mark file as ready (`pending_analysis = FALSE`, `analysis_status = 'Completed'`) +- Resumable: Can recover from crashes/restarts + +**`worker.rs`** - Query processing pipeline +- **Background worker** that processes queries with `status = 'Queued'` +- Requeues stale InProgress jobs (>10 min) +- **Stage 1**: Embed query text +- **Stage 2**: Search top-K similar vectors in Qdrant +- **Stage 3**: Fetch file metadata from MySQL (only completed files) +- **Stage 4**: Call Gemini to analyze relationships between files +- **Stage 5**: Call Gemini for final answer synthesis (strict: no speculation) +- **Stage 6**: Save results to database +- Supports cancellation checks between stages + +**`gemini_client.rs`** - Gemini API integration +- `generate_text(prompt)` - Text generation with model switching via GEMINI_MODEL env var +- `demo_text_embedding(text)` - Demo 64-dim embeddings (replace with real Gemini embeddings) +- Falls back to demo responses if GEMINI_API_KEY not set + +**`vector_db.rs`** - Qdrant client +- `ensure_files_collection(dim)` - Create 'files' collection with Cosine distance +- `upsert_point(id, vector)` - Store file embedding +- `search_top_k(vector, k)` - Find k nearest neighbors +- `delete_point(id)` - Remove file from index + +**`storage.rs`** - File storage utilities +- `storage_dir()` - Get storage path from ASTRA_STORAGE env or default `/app/storage` +- `ensure_storage_dir()` - Create storage directory if missing +- `save_file(filename, contents)` - Save file to storage +- `delete_file(path)` - Remove file from storage + +**`models.rs`** - Data structures +- `FileRecord` - File metadata (mirrors files table) +- `QueryRecord` - Query metadata (mirrors queries table) +- `QueryStatus` enum - Queued, InProgress, Completed, Cancelled, Failed + +### 2. **Web App** (`web-app/`) +React + Vite frontend with Express backend for API proxying. + +#### Backend (`server.mjs`) +- Express server that proxies API calls to rust-engine:8000 +- Serves React static build from `/dist` +- **Why needed**: Docker networking - React can't call rust-engine directly from browser + +#### Frontend (`src/`) +- `App.jsx` - Main chat interface component +- `components/ui/chat/chat-header.jsx` - Header with debug-only "Seed Demo Data" button (visible with `?debug=1`) +- Calls `/api/files/import-demo` endpoint to bulk-load ISS PDFs + +### 3. **MySQL Database** +Two tables for metadata storage: + +**`files` table** +```sql +id VARCHAR(36) PRIMARY KEY +filename TEXT NOT NULL +path TEXT NOT NULL +description TEXT +created_at DATETIME DEFAULT CURRENT_TIMESTAMP +pending_analysis BOOLEAN DEFAULT TRUE +analysis_status VARCHAR(32) DEFAULT 'Queued' +``` + +**`queries` table** +```sql +id VARCHAR(36) PRIMARY KEY +status VARCHAR(32) NOT NULL +payload JSON +result JSON +created_at DATETIME DEFAULT CURRENT_TIMESTAMP +updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +``` + +### 4. **Qdrant Vector Database** +- Collection: `files` +- Dimension: 64 (demo) - replace with real Gemini embedding dimension +- Distance: Cosine similarity +- Stores file embeddings for semantic search + +### 5. **Demo Data** (`rust-engine/demo-data/`) +~20 ISS technical PDFs organized by subsystem: +- Electrical Power System (EPS) +- Environmental Control & Life Support (ECLSS) +- Command & Data Handling (C&DH) +- Structures & Mechanisms + +## Data Flow + +### File Upload & Analysis +``` +1. User uploads PDF β†’ POST /api/files +2. API saves file to storage, inserts DB record (pending_analysis=true) +3. FileWorker claims pending file +4. Gemini 1.5 Flash generates description +5. Gemini 1.5 Pro generates vector graph data +6. Embed text β†’ upsert to Qdrant +7. Mark file as ready (pending_analysis=false) +``` + +### Query Processing +``` +1. User submits query β†’ POST /api/query/create +2. API inserts query record (status='Queued') +3. QueryWorker claims queued query +4. Embed query text +5. Search Qdrant for top-K similar files +6. Fetch file metadata from MySQL +7. Gemini analyzes relationships between files +8. Gemini synthesizes final answer (no speculation) +9. Save results to database +``` + +## Deployment + +### Development (`docker-compose.yml`) +- Local testing with hot-reload +- Bind mounts for code + +### Production (`docker-compose.prod.yml`) +- Used by GitHub Actions for deployment +- Runs rust-engine as user "1004" (github-actions) +- Docker volume: `rust-storage` β†’ `/app/storage` +- Bind mount: `/var/www/codered-astra/rust-engine/demo-data` β†’ `/app/demo-data:ro` +- Environment variables: + - `ASTRA_STORAGE=/app/storage` + - `DEMO_DATA_DIR=/app/demo-data` + - `QDRANT_URL=http://qdrant:6333` + - `GEMINI_API_KEY=` + - `DATABASE_URL=mysql://astraadmin:password@mysql:3306/astra` + +## Key Design Decisions + +### 1. **Two-Stage Analysis (Flash β†’ Pro)** +- Flash is faster/cheaper for initial description +- Pro is better for deep analysis and relationship extraction +- Enables cost-effective scaling + +### 2. **Resumable Workers** +- Workers requeue stale jobs (>10 min in InProgress) +- Survives container restarts without data loss +- Atomic state transitions via SQL + +### 3. **Separation of Concerns** +- FileWorker: Makes files searchable +- QueryWorker: Answers user queries +- Independent scaling and failure isolation + +### 4. **Strict Answer Generation** +- Gemini prompted to not speculate +- Must state uncertainty when info is insufficient +- Prevents hallucination in critical ISS documentation + +### 5. **Demo Embeddings** +- Current: 64-dim deterministic embeddings from text hash +- Production: Replace with real Gemini text embeddings API +- Allows development/testing without embedding API credits + +## API Usage Examples + +### Upload File +```bash +curl -F "file=@document.pdf" http://localhost:3001/api/files +``` + +### Import Demo Data +```bash +curl -X POST http://localhost:3001/api/files/import-demo +``` + +### Create Query +```bash +curl -X POST http://localhost:3001/api/query/create \ + -H "Content-Type: application/json" \ + -d '{"q": "What is the voltage of the ISS main bus?", "top_k": 5}' +``` + +### Check Status +```bash +curl http://localhost:3001/api/query/status?id= +``` + +### Get Result +```bash +curl http://localhost:3001/api/query/result?id= +``` + +## Future Enhancements + +### High Priority +1. Real Gemini text embeddings (replace demo embeddings) +2. File status UI panel (show processing progress) +3. Health check endpoint (`/health`) +4. Data purge endpoint (clear all files/queries) + +### Medium Priority +1. Streaming query responses (SSE/WebSocket) +2. Query result caching +3. File chunking for large PDFs +4. User authentication + +### Low Priority +1. Multi-collection support (different document types) +2. Query history UI +3. File preview in chat +4. Export results to PDF + +## Troubleshooting + +### Storage Permission Errors +- Ensure `/app/storage` is owned by container user +- Docker volume must be writable by user 1004 in production + +### SQL Syntax Errors +- MySQL requires separate `CREATE TABLE` statements +- Cannot combine multiple DDL statements in one `sqlx::query()` + +### Qdrant Connection Issues +- Check QDRANT_URL environment variable +- Ensure qdrant service is running and healthy +- Verify network connectivity between containers + +### Worker Not Processing +- Check logs: `docker logs rust-engine` +- Verify database connectivity +- Look for stale InProgress jobs in queries/files tables + +## Demo Presentation (3 minutes) + +See `rust-engine/DEMODETAILS.md` for curated demo script with example queries. diff --git a/QUICK_REFERENCE.md b/QUICK_REFERENCE.md new file mode 100644 index 0000000..c694cfa --- /dev/null +++ b/QUICK_REFERENCE.md @@ -0,0 +1,219 @@ +# CodeRED-Astra Quick Reference + +## System Overview + +**Two-worker architecture for ISS document RAG:** + +1. **FileWorker**: Analyzes uploaded files (Flash β†’ Pro β†’ Embed β†’ Qdrant) +2. **QueryWorker**: Answers queries (Embed β†’ Search β†’ Relationships β†’ Answer) + +Both workers are **resumable** and automatically recover from crashes. + +## Core Data Flow + +``` +Upload PDF β†’ Storage β†’ MySQL (pending) β†’ FileWorker β†’ Qdrant β†’ MySQL (ready) + ↓ +User Query β†’ MySQL (queued) β†’ QueryWorker β†’ Search Qdrant β†’ Gemini β†’ Result +``` + +## Module Map + +| Module | Purpose | Key Functions | +|--------|---------|---------------| +| `main.rs` | Entry point | Spawns workers, serves API | +| `db.rs` | Database init | Creates files/queries tables | +| `api.rs` | HTTP endpoints | Upload, list, delete, query CRUD | +| `file_worker.rs` | File analysis | Flashβ†’Proβ†’embedβ†’upsert | +| `worker.rs` | Query processing | Searchβ†’relationshipsβ†’answer | +| `gemini_client.rs` | AI integration | Text generation, embeddings | +| `vector_db.rs` | Qdrant client | Upsert, search, delete | +| `storage.rs` | File management | Save/delete files | +| `models.rs` | Data structures | FileRecord, QueryRecord | + +## API Endpoints + +### Files +- `POST /api/files` - Upload file +- `POST /api/files/import-demo?force=1` - Bulk import demo PDFs +- `GET /api/files/list` - List all files with status +- `GET /api/files/delete?id=` - Delete file + +### Queries +- `POST /api/query/create` - Create query +- `GET /api/query/status?id=` - Check status +- `GET /api/query/result?id=` - Get result +- `GET /api/query/cancel?id=` - Cancel query + +## Database Schema + +### files +- `id` - UUID primary key +- `filename` - Original filename +- `path` - Storage path +- `description` - Gemini Flash description +- `pending_analysis` - FALSE when ready for search +- `analysis_status` - Queued/InProgress/Completed/Failed + +### queries +- `id` - UUID primary key +- `status` - Queued/InProgress/Completed/Cancelled/Failed +- `payload` - JSON query params `{"q": "...", "top_k": 5}` +- `result` - JSON result `{"summary": "...", "related_files": [...], "relationships": "...", "final_answer": "..."}` + +## Environment Variables + +### Required +- `GEMINI_API_KEY` - Gemini API key +- `DATABASE_URL` - MySQL connection string +- `QDRANT_URL` - Qdrant URL (default: http://qdrant:6333) + +### Optional +- `ASTRA_STORAGE` - Storage directory (default: /app/storage) +- `DEMO_DATA_DIR` - Demo data directory (default: /app/demo-data) +- `GEMINI_MODEL` - Override Gemini model (default: gemini-1.5-pro) + +## Worker States + +### FileWorker +1. **Queued** - File uploaded, awaiting processing +2. **InProgress** - Currently being analyzed +3. **Completed** - Ready for search (pending_analysis=FALSE) +4. **Failed** - Error during processing + +### QueryWorker +1. **Queued** - Query created, awaiting processing +2. **InProgress** - Currently searching/analyzing +3. **Completed** - Result available +4. **Cancelled** - User cancelled +5. **Failed** - Error during processing + +## Gemini Prompts + +### FileWorker Stage 1 (Flash) +``` +Describe the file '{filename}' and extract all key components, keywords, +and details for later vectorization. Be comprehensive and factual. +``` + +### FileWorker Stage 2 (Pro) +``` +Given the file '{filename}' and its description: {desc} +Generate a set of vector graph data (keywords, use cases, relationships) +that can be used for broad and precise search. Only include what is +directly supported by the file. +``` + +### QueryWorker Stage 4 (Relationships) +``` +You are an assistant analyzing relationships STRICTLY within the provided files. +Query: {query} +Files: {file_list} +Tasks: +1) Summarize key details from the files relevant to the query. +2) Describe relationships and linkages strictly supported by these files. +3) List important follow-up questions that could be answered only using the provided files. +Rules: Do NOT guess or invent. If information is insufficient in the files, explicitly state that. +``` + +### QueryWorker Stage 5 (Final Answer) +``` +You are to compose a final answer to the user query using only the information from the files. +Query: {query} +Files considered: {file_list} +Relationship analysis: {relationships} +Requirements: +- Use only information present in the files and analysis above. +- If the answer is uncertain or cannot be determined from the files, clearly state that limitation. +- Avoid speculation or assumptions. +Provide a concise, structured answer. +``` + +## Docker Architecture + +### Services +- **rust-engine** - Warp API + workers (port 8000) +- **web-app** - Express + React (port 3001) +- **mysql** - MySQL 9.1 (port 3306) +- **qdrant** - Qdrant vector DB (port 6333) +- **phpmyadmin** - DB admin UI (port 8080) + +### Volumes (Production) +- `rust-storage:/app/storage` - File storage (writable) +- `/var/www/codered-astra/rust-engine/demo-data:/app/demo-data:ro` - Demo PDFs (read-only) +- `~/astra-logs:/var/log` - Log files + +## Common Issues + +### 1. SQL Syntax Error +**Problem**: `error near 'CREATE TABLE'` +**Cause**: Multiple CREATE TABLE in one query +**Fix**: Split into separate `sqlx::query()` calls + +### 2. Permission Denied +**Problem**: `Permission denied (os error 13)` +**Cause**: Container user can't write to storage +**Fix**: Use Docker volume, ensure ownership matches container user + +### 3. Worker Not Processing +**Problem**: Files/queries stuck in Queued +**Cause**: Worker crashed or not started +**Fix**: Check logs, ensure workers spawned in main.rs + +### 4. Qdrant Connection Failed +**Problem**: `qdrant upsert/search failed` +**Cause**: Qdrant not running or wrong URL +**Fix**: Verify QDRANT_URL, check qdrant container health + +## Development Commands + +```bash +# Build and run locally +cd rust-engine +cargo build +cargo run + +# Check code +cargo check + +# Run with logs +RUST_LOG=info cargo run + +# Docker compose (dev) +docker-compose up --build + +# Docker compose (production) +docker-compose -f docker-compose.prod.yml up -d + +# View logs +docker logs rust-engine -f + +# Rebuild single service +docker-compose build rust-engine +docker-compose up -d rust-engine +``` + +## Testing Flow + +1. Start services: `docker-compose up -d` +2. Import demo data: `curl -X POST http://localhost:3001/api/files/import-demo` +3. Wait for FileWorker to complete (~30 seconds for 20 files) +4. Check file status: `curl http://localhost:3001/api/files/list` +5. Create query: `curl -X POST http://localhost:3001/api/query/create -H "Content-Type: application/json" -d '{"q": "ISS main bus voltage", "top_k": 5}'` +6. Check status: `curl http://localhost:3001/api/query/status?id=` +7. Get result: `curl http://localhost:3001/api/query/result?id=` + +## Performance Notes + +- FileWorker: ~1-2 sec per file (demo embeddings) +- QueryWorker: ~3-5 sec per query (search + 2 Gemini calls) +- Qdrant search: <100ms for 1000s of vectors +- MySQL queries: <10ms for simple selects + +## Security Considerations + +- Store GEMINI_API_KEY in GitHub Secrets (production) +- Use environment variables for all credentials +- Don't commit `.env` files +- Restrict phpmyadmin to internal network only +- Use HTTPS in production deployment diff --git a/README.md b/README.md new file mode 100644 index 0000000..b0b6190 --- /dev/null +++ b/README.md @@ -0,0 +1,75 @@ +# CodeRED-Astra πŸš€ + +A hackathon-ready project with React frontend and Rust backend engine. + +## Quick Start + +```bash +# 1. Setup environment +cp .env.example .env +# Edit .env with your credentials + +# 2. Start everything with Docker +docker-compose up --build + +# 3. Access your app +# Frontend: http://localhost +# API: http://localhost:8000 +# Database Admin: http://127.0.0.1:8080 +``` + +## Development + +**Frontend (React + Vite)**: +```bash +cd web-app +npm install +npm run dev # http://localhost:5173 +``` + +**Backend (Rust)**: +```bash +cd rust-engine +cargo run # http://localhost:8000 +``` + +## Architecture + +- **Frontend**: React 18 + Vite + Tailwind CSS +- **Backend**: Rust + Warp + SQLx +- **Database**: MySQL 8.0 + phpMyAdmin +- **API**: RESTful endpoints with CORS enabled +- **Docker**: Full containerization for easy deployment + +## Project Structure + +``` +β”œβ”€β”€ web-app/ # React frontend +β”‚ β”œβ”€β”€ src/ +β”‚ β”‚ β”œβ”€β”€ App.jsx # Main component +β”‚ β”‚ └── main.jsx # Entry point +β”‚ └── Dockerfile +β”œβ”€β”€ rust-engine/ # Rust backend +β”‚ β”œβ”€β”€ src/ +β”‚ β”‚ └── main.rs # API server +β”‚ └── Dockerfile +β”œβ”€β”€ docker-compose.yml # Full stack orchestration +└── .env.example # Environment template +``` + +## Team Workflow + +- **Frontend devs**: Work in `web-app/src/`, use `/api/*` for backend calls +- **Backend devs**: Work in `rust-engine/src/`, add endpoints to main.rs +- **Database**: Access phpMyAdmin at http://127.0.0.1:8080 + +## Features + +βœ… Hot reload for both frontend and backend +βœ… Automatic API proxying from React to Rust +βœ… Database connection with graceful fallback +βœ… CORS configured for cross-origin requests +βœ… Production-ready Docker containers +βœ… Health monitoring and status dashboard + +Ready for your hackathon! See `DEVELOPMENT.md` for detailed setup instructions. diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..8e4c091 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,72 @@ +services: + web-app: + image: ghcr.io/${REPO_NAME_LOWER}/web-app:${IMAGE_TAG} + restart: always + ports: + - "127.0.0.1:3033:3000" + environment: + - DATABASE_URL=mysql://${MYSQL_USER}:${MYSQL_PASSWORD}@mysql:3306/${MYSQL_DATABASE} + - RUST_ENGINE_URL=http://rust-engine:8000 + - GEMINI_API_KEY=${GEMINI_API_KEY} + volumes: + - /home/github-actions/codered-astra/rust-engine/demo-data:/app/storage:ro + depends_on: + - mysql + - rust-engine + + rust-engine: + image: ghcr.io/${REPO_NAME_LOWER}/rust-engine:${IMAGE_TAG} + restart: always + environment: + - DATABASE_URL=mysql://${MYSQL_USER}:${MYSQL_PASSWORD}@mysql:3306/${MYSQL_DATABASE} + - ASTRA_STORAGE=/app/storage + - DEMO_DATA_DIR=/app/demo-data + - QDRANT_URL=http://qdrant:6333 + - GEMINI_API_KEY=${GEMINI_API_KEY} + depends_on: + - mysql + - qdrant + user: "1004" + volumes: + - ~/astra-logs:/var/log + - rust-storage:/app/storage + - /home/github-actions/codered-astra/rust-engine/demo-data:/app/demo-data:ro + + mysql: + image: mysql:8.0 + restart: always + ports: + - "45.43.2.25:3306:3306" + environment: + - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} + - MYSQL_DATABASE=${MYSQL_DATABASE} + - MYSQL_USER=${MYSQL_USER} + - MYSQL_PASSWORD=${MYSQL_PASSWORD} + volumes: + - mysql-data:/var/lib/mysql + + phpmyadmin: + image: phpmyadmin/phpmyadmin + restart: always + ports: + - "127.0.0.1:8080:80" + environment: + - PMA_HOST=mysql + depends_on: + - mysql + + qdrant: + image: qdrant/qdrant:latest + restart: unless-stopped + ports: + - "127.0.0.1:6333:6333" + volumes: + - qdrant-data:/qdrant/storage + environment: + - QDRANT__SERVICE__GRPC_PORT=6334 + # expose to rust-engine via service name 'qdrant' + +volumes: + mysql-data: + qdrant-data: + rust-storage: diff --git a/frontend/README.md b/frontend/README.md deleted file mode 100644 index 8b13789..0000000 --- a/frontend/README.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/frontend/src/components/ui/button/schematic-button.jsx b/frontend/src/components/ui/button/schematic-button.jsx deleted file mode 100644 index 5067565..0000000 --- a/frontend/src/components/ui/button/schematic-button.jsx +++ /dev/null @@ -1,33 +0,0 @@ -import React, { forwardRef, useRef } from "react"; -import { motion } from "motion/react"; - -// Hidden file input that exposes an open() method via ref -const SchematicButton = forwardRef(function SchematicButton({ onFiles }, ref) { - const inputRef = useRef(null); - - React.useImperativeHandle(ref, () => ({ - open: () => inputRef.current && inputRef.current.click(), - })); - - function handleFiles(e) { - const files = Array.from(e.target.files || []); - if (files.length === 0) return; - if (onFiles) onFiles(files); - if (inputRef.current) inputRef.current.value = null; - } - - return ( - - ); -}); - -export default SchematicButton; diff --git a/frontend/src/components/ui/file/file-list.jsx b/frontend/src/components/ui/file/file-list.jsx deleted file mode 100644 index fff3544..0000000 --- a/frontend/src/components/ui/file/file-list.jsx +++ /dev/null @@ -1,83 +0,0 @@ -import React, { useRef, useState } from "react"; -import SchematicButton from "src/components/ui/button/schematic-button"; -import { motion } from "motion/react"; -import { Menu } from "lucide-react"; -import { X } from "lucide-react"; -import { FilePlus2 } from "lucide-react"; - -export default function FileList() { - const pickerRef = useRef(null); - const [open, setOpen] = useState(false); - const [files, setFiles] = useState([]); - - function handleAdd() { - if (pickerRef.current && pickerRef.current.open) pickerRef.current.open(); - } - - function handleFiles(selected) { - setFiles((s) => [...s, ...selected]); - setOpen(true); - } - - function removeFile(i) { - setFiles((s) => s.filter((_, idx) => idx !== i)); - } - - return ( -
-
- setOpen((v) => !v)} - className="p-2 rounded-xl bg-gray-700 border-2 border-gray-600" - aria-expanded={open} - whileHover={{ scale: 1.1 }} - whileTab={{ scale: 0.9 }} - > - {open ? : } - -
- - {open && ( -
-
-
-
Files
-
- - - - -
-
- -
- {files.length === 0 ? ( -
No files added
- ) : ( - files.map((f, i) => ( -
- {f.name} - -
- )) - )} -
-
-
- )} -
- ); -} diff --git a/rust-engine/Cargo.lock b/rust-engine/Cargo.lock new file mode 100644 index 0000000..b5c21c0 --- /dev/null +++ b/rust-engine/Cargo.lock @@ -0,0 +1,3439 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "adobe-cmap-parser" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d3da9d617508ab8102c22f05bd772fc225ecb4fde431e38a45284e5c129a4bc" +dependencies = [ + "pom 1.1.0", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base-x" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" + +[[package]] +name = "bitflags" +version = "2.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +dependencies = [ + "serde", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bstr" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cc" +version = "1.2.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link 0.2.1", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const_fn" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f8a2ca5ac02d09563609681103aada9e1777d54fc57a5acd7a41404f9c93b6e" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "discard" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + +[[package]] +name = "encoding" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" +dependencies = [ + "encoding-index-japanese", + "encoding-index-korean", + "encoding-index-simpchinese", + "encoding-index-singlebyte", + "encoding-index-tradchinese", +] + +[[package]] +name = "encoding-index-japanese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-korean" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-simpchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-singlebyte" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-tradchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding_index_tests" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "euclid" +version = "0.20.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bb7ef65b3777a325d1eeefefab5b6d4959da54747e33bd6258e789640f307ad" +dependencies = [ + "num-traits", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "find-msvc-tools" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" + +[[package]] +name = "flate2" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "headers" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" +dependencies = [ + "base64", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1 0.10.6", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots 1.0.3", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +dependencies = [ + "equivalent", + "hashbrown 0.16.0", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "libredox" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +dependencies = [ + "bitflags", + "libc", + "redox_syscall", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "lopdf" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de0f69c40d6dbc68ebac4bf5aec3d9978e094e22e29fcabd045acd9cec74a9dc" +dependencies = [ + "encoding", + "flate2", + "itoa", + "linked-hash-map", + "log", + "pom 3.4.0", + "time", + "weezl", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "memchr", + "mime", + "spin", + "version_check", +] + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "openssl" +version = "0.10.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ad14dd45412269e1a30f52ad8f0664f0f4f4a89ee8fe28c3b3527021ebb654" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "pdf-extract" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f21fc45e1b40af7e6c7ca32af35464c1ea7a92e5d2e1465d08c8389e033240" +dependencies = [ + "adobe-cmap-parser", + "encoding", + "euclid", + "linked-hash-map", + "lopdf", + "postscript", + "type1-encoding-parser", + "unicode-normalization", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "pom" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60f6ce597ecdcc9a098e7fddacb1065093a3d66446fa16c675e7e71d1b5c28e6" + +[[package]] +name = "pom" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c972d8f86e943ad532d0b04e8965a749ad1d18bb981a9c7b3ae72fe7fd7744b" +dependencies = [ + "bstr", +] + +[[package]] +name = "postscript" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78451badbdaebaf17f053fd9152b3ffb33b516104eacb45e7864aaa9c712f306" + +[[package]] +name = "potential_utf" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" + +[[package]] +name = "reqwest" +version = "0.12.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 1.0.3", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rsa" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rust-engine" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "bytes", + "chrono", + "dotenvy", + "futures-util", + "lazy_static", + "pdf-extract", + "reqwest", + "serde", + "serde_json", + "sqlx", + "tokio", + "tokio-util", + "tracing", + "tracing-subscriber", + "uuid", + "warp", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "751e04a496ca00bb97a5e043158d23d66b5aabf2e1d5aa2a0aaebb1aafe6f82c" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770" +dependencies = [ + "sha1_smol", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64", + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap", + "log", + "memchr", + "once_cell", + "percent-encoding", + "rustls", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", + "webpki-roots 0.26.11", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.107", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.107", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "bytes", + "chrono", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.5", + "rsa", + "serde", + "sha1 0.10.6", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.5", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "chrono", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "standback" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e113fb6f3de07a243d434a56ec6f186dfd51cb08448239fe7bcae73f87ff28ff" +dependencies = [ + "version_check", +] + +[[package]] +name = "stdweb" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" +dependencies = [ + "discard", + "rustc_version", + "stdweb-derive", + "stdweb-internal-macros", + "stdweb-internal-runtime", + "wasm-bindgen", +] + +[[package]] +name = "stdweb-derive" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "serde_derive", + "syn 1.0.109", +] + +[[package]] +name = "stdweb-internal-macros" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" +dependencies = [ + "base-x", + "proc-macro2", + "quote", + "serde", + "serde_derive", + "serde_json", + "sha1 0.6.1", + "syn 1.0.109", +] + +[[package]] +name = "stdweb-internal-runtime" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a26dbd934e5451d21ef060c018dae56fc073894c5a7896f882928a76e6d081b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4752a97f8eebd6854ff91f1c1824cd6160626ac4bd44287f7f4ea2035a02a242" +dependencies = [ + "const_fn", + "libc", + "standback", + "stdweb", + "time-macros", + "version_check", + "winapi", +] + +[[package]] +name = "time-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1" +dependencies = [ + "proc-macro-hack", + "time-macros-impl", +] + +[[package]] +name = "time-macros-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3c141a1b43194f3f56a1411225df8646c55781d5f26db825b3d98507eb482f" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "standback", + "syn 1.0.109", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "type1-encoding-parser" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d6cc09e1a99c7e01f2afe4953789311a1c50baebbdac5b477ecf78e2e92a5b" +dependencies = [ + "pom 1.1.0", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" + +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +dependencies = [ + "getrandom 0.3.4", + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "warp" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d06d9202adc1f15d709c4f4a2069be5428aa912cc025d6f268ac441ab066b0" +dependencies = [ + "bytes", + "futures-util", + "headers", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "log", + "mime", + "mime_guess", + "multer", + "percent-encoding", + "pin-project", + "scoped-tls", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-util", + "tower-service", + "tracing", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.107", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.3", +] + +[[package]] +name = "webpki-roots" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b130c0d2d49f8b6889abc456e795e82525204f27c42cf767cf0d7734e089b8" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "weezl" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] diff --git a/rust-engine/Cargo.toml b/rust-engine/Cargo.toml new file mode 100644 index 0000000..ccbb0b2 --- /dev/null +++ b/rust-engine/Cargo.toml @@ -0,0 +1,26 @@ +# rust-engine/Cargo.toml + +[package] +name = "rust-engine" +version = "0.1.0" +edition = "2021" + +[dependencies] +tokio = { version = "1.38.0", features = ["full"] } +warp = { version = "0.4.2", features = ["server", "multipart"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +sqlx = { version = "0.8.6", features = ["runtime-tokio-rustls", "mysql", "chrono", "uuid", "macros"] } +chrono = { version = "0.4", features = ["serde"] } +tracing = "0.1" +tracing-subscriber = "0.3" +dotenvy = "0.15.7" # Switched from unmaintained 'dotenv' +anyhow = "1.0" +uuid = { version = "1", features = ["serde", "v4"] } +reqwest = { version = "0.12.24", features = ["json", "rustls-tls"] } +async-trait = "0.1" +tokio-util = "0.7" +futures-util = "0.3" +lazy_static = "1.4" +bytes = "1.4" +pdf-extract = "0.6" diff --git a/rust-engine/DEMODETAILS.md b/rust-engine/DEMODETAILS.md new file mode 100644 index 0000000..a8a2572 --- /dev/null +++ b/rust-engine/DEMODETAILS.md @@ -0,0 +1,48 @@ +## Demo Runbook: ISS Systems (3-minute showcase) + +This demo uses ~20 public NASA PDFs covering ISS Electrical Power, ECLSS, Avionics, and Structures. They live in `rust-engine/demo-data` and are automatically ingested via the server. + +### 1) Seed demo data (one-click) + +- Trigger ingestion (cloud): POST `/api/files/import-demo` (UI button available when `?debug=1` is present) +- The backend copies PDFs into storage, inserts DB rows with `pending_analysis = true`, and the FileWorker processes them. +- Processing pipeline per file: + - Gemini Flash β†’ comprehensive description (facts/keywords/components) + - Gemini Pro β†’ deep vector graph data (keywords/use cases/relationships) + - Embed + upsert to Qdrant, mark file ready (`pending_analysis = false`) + +Tip: You can list files at `GET /api/files/list`. Ready files will start to appear as analysis completes. + +### 2) Showcase flow (suggested script) + +1. β€œWe ingested real ISS technical PDFs. The worker analyzes each file with Gemini and builds vector graph data for robust retrieval.” +2. Show the files list. Point out a couple of recognizable titles. +3. Run two queries (examples below) and open their results (the app calls `POST /api/query/create` then polls `/api/query/result`). +4. Highlight the grounded answer: β€˜related_files’, β€˜relationships’, and β€˜final_answer’ fields. +5. Call out that if info isn’t present in the PDFs, the system explicitly states uncertainty (no guessing). + +### 3) Demo queries (pick 2–3) + +- Electrical Power System (EPS) + - β€œTrace the power path from the P6 solar array to the BCDU. Where are likely ground fault points?” + - β€œWhat is the role of the DC Switching Unit in array power management?” +- ECLSS + - β€œWhich modules are part of water recovery, and how does the Oxygen Generator Assembly interface?” + - β€œSummarize the CDRA cycle and downstream subsystems it impacts.” +- C&DH / Avionics + - β€œIn the US Lab, a blue/white wire connects to MDM β€˜LAB1’. What are possible data pathways?” + - β€œDescribe the onboard LAN segments and links to MDMs.” +- Structures / Robotics + - β€œWhere does the Latching End Effector connect on S1 truss?” + - β€œWhat is the Mobile Transporter’s role in SSRMS operations?” + +### 4) Reset/refresh (optional) + +- POST `/api/files/import-demo?force=1` to overwrite by filename and re-queue analysis. + +### Appendix: Example sources + +- EPS: 20110014867, 20040171627, 19900007297, 20120002931, 20100029672 +- ECLSS: 20170008316, 20070019910, 20080039691, 20100029191, 20070019929 +- C&DH: 20000012543, 20100029690, 19950014639, 20010023477, 19980227289 +- Structures/Robotics: 20020054238, 20010035542, 20140001008, Destiny fact sheet, 20020088289 \ No newline at end of file diff --git a/rust-engine/Dockerfile b/rust-engine/Dockerfile new file mode 100644 index 0000000..cf22a44 --- /dev/null +++ b/rust-engine/Dockerfile @@ -0,0 +1,75 @@ +# syntax=docker/dockerfile:1.7 +# rust-engine/Dockerfile + +# --- Stage 1: Builder --- +# (No changes needed in this stage) +FROM rust:slim AS builder +WORKDIR /usr/src/app + +RUN apt-get update && apt-get install -y --no-install-recommends \ + pkg-config \ + libssl-dev \ + curl \ + build-essential \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +ARG RUSTUP_TOOLCHAIN= +ENV PATH="/usr/local/cargo/bin:${PATH}" + +COPY Cargo.toml Cargo.lock rust-toolchain.toml ./ + +RUN set -eux; \ + if [ -n "${RUSTUP_TOOLCHAIN}" ]; then \ + if ! rustup toolchain list | grep -q "^${RUSTUP_TOOLCHAIN}"; then \ + rustup toolchain install "${RUSTUP_TOOLCHAIN}"; \ + fi; \ + rustup default "${RUSTUP_TOOLCHAIN}"; \ + else \ + if [ -f rust-toolchain.toml ]; then \ + TOOLCHAIN=$(sed -n 's/^channel *= *"\(.*\)"/\1/p' rust-toolchain.toml | head -n1); \ + if [ -n "$TOOLCHAIN" ]; then \ + if ! rustup toolchain list | grep -q "^$TOOLCHAIN"; then \ + rustup toolchain install "$TOOLCHAIN"; \ + fi; \ + rustup default "$TOOLCHAIN"; \ + fi; \ + fi; \ + fi; \ + rustup show active-toolchain || true + +RUN mkdir -p src && echo "fn main() { println!(\"cargo cache build\"); }" > src/main.rs + +RUN --mount=type=cache,target=/usr/local/cargo/registry,sharing=locked \ + --mount=type=cache,target=/usr/local/cargo/git,sharing=locked \ + cargo fetch + +RUN rm -f src/main.rs +COPY src ./src +RUN cargo build --release --locked + +# --- Stage 2: Final, small image --- + +FROM debian:bookworm-slim +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/* + +RUN useradd --system --uid 1004 --no-create-home --shell /usr/sbin/nologin appuser + +# Copy the compiled binary from the builder stage +COPY --from=builder /usr/src/app/target/release/rust-engine /usr/local/bin/rust-engine + +# --- THIS IS THE FIX --- +# **1. Copy the demo data files from your local machine into the image.** +COPY demo-data /app/demo-data + +# **2. Create other directories and set permissions on everything.** +RUN chown appuser:appuser /usr/local/bin/rust-engine \ + && mkdir -p /var/log /app/storage \ + && touch /var/log/astra-errors.log \ + && chown -R appuser:appuser /var/log /app + +WORKDIR /app +USER appuser + +EXPOSE 8000 +ENTRYPOINT ["/bin/sh", "-c", "/usr/local/bin/rust-engine >> /var/log/astra-errors.log 2>&1"] \ No newline at end of file diff --git a/rust-engine/README.md b/rust-engine/README.md new file mode 100644 index 0000000..35e0edc --- /dev/null +++ b/rust-engine/README.md @@ -0,0 +1,89 @@ +# Rust Engine API and Worker + +## Overview + +- HTTP API (warp) under /api for file management and query lifecycle +- MySQL for metadata, Qdrant for vector similarity +- Background worker resumes queued work and re-queues stale InProgress jobs at startup + +## Environment variables + +- DATABASE_URL: mysql://USER:PASS@HOST:3306/DB +- QDRANT_URL: default +- GEMINI_API_KEY: used for Gemini content generation (optional in demo) +- DEMO_DATA_DIR: path to the folder containing PDF demo data (default resolves to `demo-data` under the repo or `/app/demo-data` in containers) +- ASTRA_STORAGE: directory for uploaded file blobs (default `/app/storage`) +- AUTO_IMPORT_DEMO: set to `false`, `0`, `off`, or `no` to disable automatic demo import at startup (defaults to `true`) + +## Endpoints (JSON) + +- POST /api/files (multipart) + - Form: file=@path + - Response: {"success": true} + +- GET /api/files/list + - Response: {"files": [{"id","filename","path","storage_url","description"}]} + +- POST /api/files/import-demo[?force=1] + - Copies PDFs from the demo directory into storage and queues them for analysis. + - Response: {"imported": N, "skipped": M, "files_found": K, "source_dir": "...", "attempted_paths": [...], "force": bool} + - `force=1` deletes prior records with the same filename before re-importing. + +- GET /api/files/delete?id= + - Response: {"deleted": true|false} + +- POST /api/query/create + - Body: {"q": "text", "top_k": 5} + - Response: {"id": "uuid"} + +- GET /api/query/status?id= + - Response: {"status": "Queued"|"InProgress"|"Completed"|"Cancelled"|"Failed"|"not_found"} + +- GET /api/query/result?id= + - Response (Completed): + { + "result": { + "summary": "Found N related files", + "related_files": [ + {"id","filename","path","description","score"} + ], + "relationships": "...", + "final_answer": "..." + } + } + +- GET /api/query/cancel?id= + - Response: {"cancelled": true} + +## Worker behavior + +- Ensures Qdrant collection exists (dim 64, cosine) +- Re-queues InProgress older than 10 minutes +- Processing stages: + 1) Set InProgress + 2) Embed query text (demo now; pluggable Gemini later) + 3) Search Qdrant top_k (default 5) + 4) Join file metadata (MySQL) + 5) Gemini step: relationship analysis (strictly from provided files) + 6) Gemini step: final answer (no speculation; say unknown if insufficient) + 7) Persist result (JSON) and set Completed + - Checks for cancellation between stages + +## Local quickstart + +1. docker compose up -d mysql qdrant +2. set env DATABASE_URL and QDRANT_URL +3. cargo run +4. (optional) import demo PDFs + - Populate a folder with PDFs under `rust-engine/demo-data` (or point `DEMO_DATA_DIR` to a custom path). The server auto-resolves common locations such as the repo root, `/app/demo-data`, and the working directory when running in Docker. When the engine boots it automatically attempts this import (can be disabled by setting `AUTO_IMPORT_DEMO=false`). + - Call the endpoint: + - POST + - Optional query `?force=1` to overwrite existing by filename. The JSON response also echoes where the engine looked (`source_dir`, `attempted_paths`) and how many PDFs were detected (`files_found`) so misconfigurations are easy to spot. Imported files are written to the shared `/app/storage` volume; the web-app container mounts this volume read-only and serves the contents at `/storage/`. + - Or run the PowerShell helper: + - `./scripts/import_demo.ps1` (adds all PDFs in demo-data) + - `./scripts/import_demo.ps1 -Force` (overwrite existing) + +## Notes + +- Replace demo embeddings with real Gemini calls for production +- Add auth to endpoints if needed (API key/JWT) diff --git a/rust-engine/demo-data/132-1-Final.pdf b/rust-engine/demo-data/132-1-Final.pdf new file mode 100644 index 0000000..ac0ce14 Binary files /dev/null and b/rust-engine/demo-data/132-1-Final.pdf differ diff --git a/rust-engine/demo-data/179225main_iss_poster_back.pdf b/rust-engine/demo-data/179225main_iss_poster_back.pdf new file mode 100644 index 0000000..9386c43 Binary files /dev/null and b/rust-engine/demo-data/179225main_iss_poster_back.pdf differ diff --git a/rust-engine/demo-data/19790004570.pdf b/rust-engine/demo-data/19790004570.pdf new file mode 100644 index 0000000..a0d22b5 Binary files /dev/null and b/rust-engine/demo-data/19790004570.pdf differ diff --git a/rust-engine/demo-data/19880012104.pdf b/rust-engine/demo-data/19880012104.pdf new file mode 100644 index 0000000..d7f48ea Binary files /dev/null and b/rust-engine/demo-data/19880012104.pdf differ diff --git a/rust-engine/demo-data/19890016674.pdf b/rust-engine/demo-data/19890016674.pdf new file mode 100644 index 0000000..0e315d6 Binary files /dev/null and b/rust-engine/demo-data/19890016674.pdf differ diff --git a/rust-engine/demo-data/19920015843.pdf b/rust-engine/demo-data/19920015843.pdf new file mode 100644 index 0000000..53065cf Binary files /dev/null and b/rust-engine/demo-data/19920015843.pdf differ diff --git a/rust-engine/demo-data/19950014639.pdf b/rust-engine/demo-data/19950014639.pdf new file mode 100644 index 0000000..5b200c2 Binary files /dev/null and b/rust-engine/demo-data/19950014639.pdf differ diff --git a/rust-engine/demo-data/20040171627.pdf b/rust-engine/demo-data/20040171627.pdf new file mode 100644 index 0000000..fe3eb42 Binary files /dev/null and b/rust-engine/demo-data/20040171627.pdf differ diff --git a/rust-engine/demo-data/20050207388.pdf b/rust-engine/demo-data/20050207388.pdf new file mode 100644 index 0000000..23ba263 Binary files /dev/null and b/rust-engine/demo-data/20050207388.pdf differ diff --git a/rust-engine/demo-data/20050210002.pdf b/rust-engine/demo-data/20050210002.pdf new file mode 100644 index 0000000..c7d6e7e Binary files /dev/null and b/rust-engine/demo-data/20050210002.pdf differ diff --git a/rust-engine/demo-data/20080014096.pdf b/rust-engine/demo-data/20080014096.pdf new file mode 100644 index 0000000..2a23fcf Binary files /dev/null and b/rust-engine/demo-data/20080014096.pdf differ diff --git a/rust-engine/demo-data/20100029672.pdf b/rust-engine/demo-data/20100029672.pdf new file mode 100644 index 0000000..d639021 Binary files /dev/null and b/rust-engine/demo-data/20100029672.pdf differ diff --git a/rust-engine/demo-data/20110014867.pdf b/rust-engine/demo-data/20110014867.pdf new file mode 100644 index 0000000..609a6df Binary files /dev/null and b/rust-engine/demo-data/20110014867.pdf differ diff --git a/rust-engine/demo-data/20120002931.pdf b/rust-engine/demo-data/20120002931.pdf new file mode 100644 index 0000000..deb27bf Binary files /dev/null and b/rust-engine/demo-data/20120002931.pdf differ diff --git a/rust-engine/demo-data/20190028718.pdf b/rust-engine/demo-data/20190028718.pdf new file mode 100644 index 0000000..d5211e8 Binary files /dev/null and b/rust-engine/demo-data/20190028718.pdf differ diff --git a/rust-engine/demo-data/20200003149.pdf b/rust-engine/demo-data/20200003149.pdf new file mode 100644 index 0000000..59ef117 Binary files /dev/null and b/rust-engine/demo-data/20200003149.pdf differ diff --git a/rust-engine/demo-data/473486main_iss_atcs_overview.pdf b/rust-engine/demo-data/473486main_iss_atcs_overview.pdf new file mode 100644 index 0000000..d742279 Binary files /dev/null and b/rust-engine/demo-data/473486main_iss_atcs_overview.pdf differ diff --git a/rust-engine/demo-data/8Mod6Prob1.pdf b/rust-engine/demo-data/8Mod6Prob1.pdf new file mode 100644 index 0000000..d719e74 Binary files /dev/null and b/rust-engine/demo-data/8Mod6Prob1.pdf differ diff --git a/rust-engine/demo-data/ICES_2023_311 final 5 15 23.pdf b/rust-engine/demo-data/ICES_2023_311 final 5 15 23.pdf new file mode 100644 index 0000000..e10f860 Binary files /dev/null and b/rust-engine/demo-data/ICES_2023_311 final 5 15 23.pdf differ diff --git a/rust-engine/demo-data/ISwSIS Software Standard_NASA-102020_Draft.docx.pdf b/rust-engine/demo-data/ISwSIS Software Standard_NASA-102020_Draft.docx.pdf new file mode 100644 index 0000000..62f2eec Binary files /dev/null and b/rust-engine/demo-data/ISwSIS Software Standard_NASA-102020_Draft.docx.pdf differ diff --git a/rust-engine/rust-toolchain.toml b/rust-engine/rust-toolchain.toml new file mode 100644 index 0000000..4386630 --- /dev/null +++ b/rust-engine/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "1.88.0" +# components = ["rustfmt", "clippy"] +# targets = ["x86_64-unknown-linux-gnu"] diff --git a/rust-engine/src/api.rs b/rust-engine/src/api.rs new file mode 100644 index 0000000..585ad40 --- /dev/null +++ b/rust-engine/src/api.rs @@ -0,0 +1,455 @@ +use crate::storage; +use crate::vector_db::QdrantClient; +use anyhow::Result; +use bytes::Buf; +use futures_util::TryStreamExt; +use serde::{Deserialize, Serialize}; +use sqlx::{MySqlPool, Row}; +use tracing::{debug, info, warn}; +use warp::{multipart::FormData, Filter, Rejection, Reply}; + +#[derive(Debug, Deserialize)] +struct DeleteQuery { + id: String, +} + +#[derive(Debug, Serialize)] +pub struct DemoImportSummary { + pub imported: usize, + pub skipped: usize, + pub files_found: usize, + #[serde(skip_serializing_if = "Option::is_none")] + pub source_dir: Option, + pub attempted_paths: Vec, + pub force: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +pub async fn perform_demo_import(force: bool, pool: &MySqlPool) -> Result { + use anyhow::Context; + use std::fs; + use std::path::PathBuf; + + let demo_dir_setting = + std::env::var("DEMO_DATA_DIR").unwrap_or_else(|_| "demo-data".to_string()); + info!(force, requested_dir = %demo_dir_setting, "demo import requested"); + + let base = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); + + // Build a list of plausible demo-data locations so local runs and containers both work. + let mut candidates: Vec = Vec::new(); + let configured = PathBuf::from(&demo_dir_setting); + let mut push_candidate = |path: PathBuf| { + if !candidates.iter().any(|existing| existing == &path) { + candidates.push(path); + } + }; + + push_candidate(base.join(&configured)); + push_candidate(PathBuf::from(&demo_dir_setting)); + push_candidate(base.join("rust-engine").join(&configured)); + push_candidate(base.join("rust-engine").join("demo-data")); + push_candidate(base.join("demo-data")); + if let Ok(exe_path) = std::env::current_exe() { + if let Some(exe_dir) = exe_path.parent() { + push_candidate(exe_dir.join(&configured)); + push_candidate(exe_dir.join("demo-data")); + push_candidate(exe_dir.join("rust-engine").join(&configured)); + } + } + + let mut attempted: Vec = Vec::new(); + let mut resolved_dir: Option = None; + for candidate in candidates { + debug!(candidate = %candidate.display(), "evaluating demo import path candidate"); + if candidate.exists() && candidate.is_dir() { + resolved_dir = Some(candidate.clone()); + break; + } + attempted.push(candidate); + } + + let attempted_paths: Vec = attempted.iter().map(|p| p.display().to_string()).collect(); + let mut summary = DemoImportSummary { + imported: 0, + skipped: 0, + files_found: 0, + source_dir: resolved_dir.as_ref().map(|p| p.display().to_string()), + attempted_paths, + force, + error: None, + }; + + let src_dir = match resolved_dir { + Some(path) => path, + None => { + summary.error = + Some("demo dir not found; set DEMO_DATA_DIR or bind mount demo PDFs".to_string()); + warn!( + attempted_paths = ?summary.attempted_paths, + "demo import skipped; source directory not found" + ); + return Ok(summary); + } + }; + + summary.source_dir = Some(src_dir.display().to_string()); + info!( + source = %summary.source_dir.as_deref().unwrap_or_default(), + attempted_paths = ?summary.attempted_paths, + "demo import source resolved" + ); + + for entry in fs::read_dir(&src_dir).with_context(|| format!("reading {:?}", src_dir))? { + let entry = entry.with_context(|| format!("reading entry in {:?}", src_dir))?; + let path = entry.path(); + if path + .extension() + .and_then(|e| e.to_str()) + .map(|e| e.eq_ignore_ascii_case("pdf")) + .unwrap_or(false) + { + summary.files_found += 1; + let filename = path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("unknown.pdf") + .to_string(); + + if !force { + if let Some(_) = sqlx::query("SELECT id FROM files WHERE filename = ?") + .bind(&filename) + .fetch_optional(pool) + .await? + { + summary.skipped += 1; + info!(%filename, "skipping demo import; already present"); + continue; + } + } + + let data = fs::read(&path).with_context(|| format!("reading {:?}", path))?; + let stored_path = storage::save_file(&filename, &data)?; + info!(%filename, dest = %stored_path.to_string_lossy(), "demo file copied to storage"); + + let id = uuid::Uuid::new_v4().to_string(); + + if force { + sqlx::query("DELETE FROM files WHERE filename = ?") + .bind(&filename) + .execute(pool) + .await?; + info!(%filename, "existing file records removed due to force import"); + } + + sqlx::query("INSERT INTO files (id, filename, path, description, pending_analysis, analysis_status) VALUES (?, ?, ?, ?, ?, 'Queued')") + .bind(&id) + .bind(&filename) + .bind(stored_path.to_string_lossy().to_string()) + .bind(Option::::None) + .bind(true) + .execute(pool) + .await?; + info!(%filename, file_id = %id, "demo file inserted into database"); + + info!(%filename, file_id = %id, "demo file queued for analysis by worker"); + + summary.imported += 1; + } else { + debug!(path = %path.display(), "skipping non-PDF entry during demo import"); + } + } + + let source_label = summary.source_dir.as_deref().unwrap_or("unknown"); + + if summary.files_found == 0 { + warn!(source = %source_label, "demo import located zero PDFs"); + } + + info!( + source = %source_label, + files_found = summary.files_found, + attempted_paths = ?summary.attempted_paths, + imported = summary.imported, + skipped = summary.skipped, + force, + "demo import completed" + ); + + Ok(summary) +} + +pub fn routes(pool: MySqlPool) -> impl Filter + Clone { + let pool_filter = warp::any().map(move || pool.clone()); + + // Import demo files from demo-data directory + let import_demo = warp::path!("files" / "import-demo") + .and(warp::post()) + .and( + warp::query::>() + .or(warp::any().map(|| std::collections::HashMap::new())) + .unify(), + ) + .and(pool_filter.clone()) + .and_then(handle_import_demo); + + // Upload file + let upload = warp::path("files") + .and(warp::post()) + .and(warp::multipart::form().max_length(50_000_000)) // 50MB per part default; storage is filesystem-backed + .and(pool_filter.clone()) + .and_then(handle_upload); + + // Delete file + let delete = warp::path!("files" / "delete") + .and(warp::get()) + .and(warp::query::()) + .and(pool_filter.clone()) + .and_then(handle_delete); + + // List files + let list = warp::path!("files" / "list") + .and(warp::get()) + .and(pool_filter.clone()) + .and_then(handle_list); + + // Create query + let create_q = warp::path!("query" / "create") + .and(warp::post()) + .and(warp::body::json()) + .and(pool_filter.clone()) + .and_then(handle_create_query); + + // Query status + let status = warp::path!("query" / "status") + .and(warp::get()) + .and(warp::query::()) + .and(pool_filter.clone()) + .and_then(handle_query_status); + + // Query result + let result = warp::path!("query" / "result") + .and(warp::get()) + .and(warp::query::()) + .and(pool_filter.clone()) + .and_then(handle_query_result); + + // Cancel + let cancel = warp::path!("query" / "cancel") + .and(warp::get()) + .and(warp::query::()) + .and(pool_filter.clone()) + .and_then(handle_cancel_query); + + let api = upload + .or(import_demo) + .or(delete) + .or(list) + .or(create_q) + .or(status) + .or(result) + .or(cancel); + warp::path("api").and(api) +} + +async fn handle_upload(mut form: FormData, pool: MySqlPool) -> Result { + let mut created_files = Vec::new(); + while let Some(field) = form.try_next().await.map_err(|_| warp::reject())? { + let _name = field.name().to_string(); + let filename = field + .filename() + .map(|s| s.to_string()) + .unwrap_or_else(|| format!("upload-{}", uuid::Uuid::new_v4())); + + // Read stream of Buf into a Vec + let data = field + .stream() + .map_ok(|mut buf| { + let mut v = Vec::new(); + while buf.has_remaining() { + let chunk = buf.chunk(); + v.extend_from_slice(chunk); + let n = chunk.len(); + buf.advance(n); + } + v + }) + .try_fold(Vec::new(), |mut acc, chunk_vec| async move { + acc.extend_from_slice(&chunk_vec); + Ok(acc) + }) + .await + .map_err(|_| warp::reject())?; + + // Save file + let path = storage::save_file(&filename, &data).map_err(|_| warp::reject())?; + + // Insert file record with pending_analysis = true, description = NULL + let id = uuid::Uuid::new_v4().to_string(); + sqlx::query("INSERT INTO files (id, filename, path, description, pending_analysis, analysis_status) VALUES (?, ?, ?, ?, ?, 'Queued')") + .bind(&id) + .bind(&filename) + .bind(path.to_str().unwrap()) + .bind(Option::::None) + .bind(true) + .execute(&pool) + .await + .map_err(|e| { + tracing::error!("DB insert error: {}", e); + warp::reject() + })?; + created_files.push(serde_json::json!({ + "id": id, + "filename": filename, + "pending_analysis": true, + "analysis_status": "Queued" + })); + } + + Ok(warp::reply::json(&serde_json::json!({ + "uploaded": created_files.len(), + "files": created_files + }))) +} + +async fn handle_import_demo( + params: std::collections::HashMap, + pool: MySqlPool, +) -> Result { + let force = params + .get("force") + .map(|v| v == "1" || v.eq_ignore_ascii_case("true")) + .unwrap_or(false); + + match perform_demo_import(force, &pool).await { + Ok(summary) => Ok(warp::reply::json(&summary)), + Err(err) => { + tracing::error!(error = %err, "demo import failed"); + let fallback = DemoImportSummary { + imported: 0, + skipped: 0, + files_found: 0, + source_dir: None, + attempted_paths: Vec::new(), + force, + error: Some(err.to_string()), + }; + Ok(warp::reply::json(&fallback)) + } + } +} + +async fn handle_delete(q: DeleteQuery, pool: MySqlPool) -> Result { + if let Some(row) = sqlx::query("SELECT path FROM files WHERE id = ?") + .bind(&q.id) + .fetch_optional(&pool) + .await + .map_err(|_| warp::reject())? + { + let path: String = row.get("path"); + let _ = storage::delete_file(std::path::Path::new(&path)); + // Remove from Qdrant + let qdrant_url = + std::env::var("QDRANT_URL").unwrap_or_else(|_| "http://qdrant:6333".to_string()); + let qdrant = QdrantClient::new(&qdrant_url); + let _ = qdrant.delete_point(&q.id).await; + let _ = sqlx::query("DELETE FROM files WHERE id = ?") + .bind(&q.id) + .execute(&pool) + .await; + return Ok(warp::reply::json(&serde_json::json!({"deleted": true}))); + } + Ok(warp::reply::json(&serde_json::json!({"deleted": false}))) +} + +async fn handle_list(pool: MySqlPool) -> Result { + let rows = sqlx::query("SELECT id, filename, path, description, pending_analysis, analysis_status FROM files ORDER BY created_at DESC LIMIT 500") + .fetch_all(&pool) + .await + .map_err(|e| { + tracing::error!("DB list error: {}", e); + warp::reject() + })?; + let files: Vec = rows + .into_iter() + .map(|r| { + let id: String = r.get("id"); + let filename: String = r.get("filename"); + let path: String = r.get("path"); + let description: Option = r.get("description"); + let pending: bool = r.get("pending_analysis"); + let status: Option = r.try_get("analysis_status").ok(); + let storage_url = format!("/storage/{}", filename); + serde_json::json!({ + "id": id, + "filename": filename, + "path": path, + "storage_url": storage_url, + "description": description, + "pending_analysis": pending, + "analysis_status": status + }) + }) + .collect(); + + Ok(warp::reply::json(&serde_json::json!({"files": files}))) +} + +async fn handle_create_query( + body: serde_json::Value, + pool: MySqlPool, +) -> Result { + // Insert query as queued, worker will pick it up + let id = uuid::Uuid::new_v4().to_string(); + let payload = body; + sqlx::query("INSERT INTO queries (id, status, payload) VALUES (?, 'Queued', ?)") + .bind(&id) + .bind(payload) + .execute(&pool) + .await + .map_err(|e| { + tracing::error!("DB insert query error: {}", e); + warp::reject() + })?; + + Ok(warp::reply::json(&serde_json::json!({"id": id}))) +} + +async fn handle_query_status(q: DeleteQuery, pool: MySqlPool) -> Result { + if let Some(row) = sqlx::query("SELECT status FROM queries WHERE id = ?") + .bind(&q.id) + .fetch_optional(&pool) + .await + .map_err(|_| warp::reject())? + { + let status: String = row.get("status"); + return Ok(warp::reply::json(&serde_json::json!({"status": status}))); + } + Ok(warp::reply::json( + &serde_json::json!({"status": "not_found"}), + )) +} + +async fn handle_query_result(q: DeleteQuery, pool: MySqlPool) -> Result { + if let Some(row) = sqlx::query("SELECT result FROM queries WHERE id = ?") + .bind(&q.id) + .fetch_optional(&pool) + .await + .map_err(|_| warp::reject())? + { + let result: Option = row.get("result"); + return Ok(warp::reply::json(&serde_json::json!({"result": result}))); + } + Ok(warp::reply::json(&serde_json::json!({"result": null}))) +} + +async fn handle_cancel_query(q: DeleteQuery, pool: MySqlPool) -> Result { + // Mark as cancelled; worker must check status before heavy steps + sqlx::query("UPDATE queries SET status = 'Cancelled' WHERE id = ?") + .bind(&q.id) + .execute(&pool) + .await + .map_err(|_| warp::reject())?; + Ok(warp::reply::json(&serde_json::json!({"cancelled": true}))) +} diff --git a/rust-engine/src/db.rs b/rust-engine/src/db.rs new file mode 100644 index 0000000..3245814 --- /dev/null +++ b/rust-engine/src/db.rs @@ -0,0 +1,42 @@ +use sqlx::MySqlPool; +use tracing::info; + +pub async fn init_db(database_url: &str) -> Result { + let pool = MySqlPool::connect(database_url).await?; + + // Create tables if they don't exist. Simple schema for demo/hackathon use. + // Note: MySQL requires separate statements for each CREATE TABLE + sqlx::query( + r#" + CREATE TABLE IF NOT EXISTS files ( + id VARCHAR(36) PRIMARY KEY, + filename TEXT NOT NULL, + path TEXT NOT NULL, + description TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + pending_analysis BOOLEAN DEFAULT TRUE, + analysis_status VARCHAR(32) DEFAULT 'Queued' + ) + "#, + ) + .execute(&pool) + .await?; + + sqlx::query( + r#" + CREATE TABLE IF NOT EXISTS queries ( + id VARCHAR(36) PRIMARY KEY, + status VARCHAR(32) NOT NULL, + payload JSON, + result JSON, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) + "#, + ) + .execute(&pool) + .await?; + + info!("Database initialized"); + Ok(pool) +} diff --git a/rust-engine/src/file_worker.rs b/rust-engine/src/file_worker.rs new file mode 100644 index 0000000..4316d67 --- /dev/null +++ b/rust-engine/src/file_worker.rs @@ -0,0 +1,226 @@ +use crate::gemini_client::{demo_text_embedding, generate_text_with_model, DEMO_EMBED_DIM}; +use crate::vector; +use crate::vector_db::QdrantClient; +use anyhow::{anyhow, Context, Result}; +use pdf_extract::extract_text; +use sqlx::MySqlPool; +use std::path::PathBuf; +use tracing::{error, info, warn}; + +pub struct FileWorker { + pool: MySqlPool, + qdrant: QdrantClient, +} + +impl FileWorker { + pub fn new(pool: MySqlPool) -> Self { + let qdrant_url = + std::env::var("QDRANT_URL").unwrap_or_else(|_| "http://qdrant:6333".to_string()); + let qdrant = QdrantClient::new(&qdrant_url); + Self { pool, qdrant } + } + + pub async fn run(&self) { + info!("FileWorker starting"); + if let Err(e) = self.qdrant.ensure_files_collection(DEMO_EMBED_DIM).await { + error!("Failed to ensure Qdrant collection: {}", e); + } + loop { + match self.fetch_and_claim().await { + Ok(Some(fid)) => { + info!("Processing file {}", fid); + if let Err(e) = self.process_file(&fid).await { + error!("Error processing file {}: {}", fid, e); + if let Err(mark_err) = self.mark_failed(&fid, &format!("{}", e)).await { + error!("Failed to mark file {} as failed: {}", fid, mark_err); + } + } + } + Ok(None) => { + tokio::time::sleep(std::time::Duration::from_secs(2)).await; + } + Err(e) => { + error!("FileWorker fetch error: {}", e); + tokio::time::sleep(std::time::Duration::from_secs(5)).await; + } + } + } + } + + async fn fetch_and_claim(&self) -> Result> { + // Claim files that are queued or stuck in progress for >10min + if let Some(row) = sqlx::query( + "SELECT id FROM files WHERE (analysis_status = 'Queued' OR (analysis_status = 'InProgress' AND created_at < (NOW() - INTERVAL 10 MINUTE))) AND pending_analysis = TRUE LIMIT 1" + ) + .fetch_optional(&self.pool) + .await? { + use sqlx::Row; + let id: String = row.get("id"); + // Mark as in-progress + let _ = sqlx::query("UPDATE files SET analysis_status = 'InProgress' WHERE id = ?") + .bind(&id) + .execute(&self.pool) + .await?; + Ok(Some(id)) + } else { + Ok(None) + } + } + + async fn process_file(&self, file_id: &str) -> Result<()> { + use sqlx::Row; + let row = sqlx::query("SELECT filename, path FROM files WHERE id = ?") + .bind(file_id) + .fetch_one(&self.pool) + .await?; + let filename: String = row.get("filename"); + let path: String = row.get("path"); + + let (file_excerpt, truncated) = match extract_file_excerpt(&path).await { + Ok(res) => res, + Err(err) => { + error!(file_id, %filename, %path, error = ?err, "failed to extract text from file; continuing with filename only"); + (String::new(), false) + } + }; + if file_excerpt.is_empty() { + warn!(file_id, %filename, %path, "extracted excerpt is empty; prompts may lack context"); + } + + let excerpt_note = if truncated { + "(excerpt truncated for prompt size)" + } else { + "" + }; + + // Stage 1: Gemini 2.5 Flash for description + let desc_prompt = format!( + "You are reviewing the PDF file '{filename}'. Use the following extracted text {excerpt_note} to produce a concise, factual description and key highlights that will help downstream search and reasoning.\n\n--- BEGIN EXCERPT ---\n{}\n--- END EXCERPT ---", + file_excerpt + ); + let desc = generate_text_with_model("gemini-2.5-flash", &desc_prompt) + .await + .unwrap_or_else(|e| format!("[desc error: {}]", e)); + sqlx::query( + "UPDATE files SET description = ?, analysis_status = 'InProgress' WHERE id = ?", + ) + .bind(&desc) + .bind(file_id) + .execute(&self.pool) + .await?; + + // Stage 2: Gemini 2.5 Pro for deep vector graph data + let vector_prompt = format!( + "You are constructing vector search metadata for the PDF file '{filename}'.\nCurrent description: {desc}\nUse the extracted text {excerpt_note} below to derive precise keywords, thematic clusters, and relationships that are explicitly supported by the content. Provide richly structured bullet points grouped by themes.\n\n--- BEGIN EXCERPT ---\n{}\n--- END EXCERPT ---", + file_excerpt + ); + let vector_graph = generate_text_with_model("gemini-2.5-pro", &vector_prompt) + .await + .unwrap_or_else(|e| format!("[vector error: {}]", e)); + + // Stage 3: Embed and upsert to Qdrant + let emb = demo_text_embedding(&vector_graph).await?; + match self.qdrant.upsert_point(file_id, emb.clone()).await { + Ok(_) => { + let _ = vector::store_embedding(file_id, emb.clone()); + } + Err(err) => { + error!("Qdrant upsert failed for {}: {}", file_id, err); + let _ = vector::store_embedding(file_id, emb); + } + } + + // Mark file as ready + sqlx::query( + "UPDATE files SET pending_analysis = FALSE, analysis_status = 'Completed' WHERE id = ?", + ) + .bind(file_id) + .execute(&self.pool) + .await?; + Ok(()) + } + + async fn mark_failed(&self, file_id: &str, reason: &str) -> Result<()> { + sqlx::query( + "UPDATE files SET analysis_status = 'Failed', pending_analysis = TRUE WHERE id = ?", + ) + .bind(file_id) + .execute(&self.pool) + .await?; + sqlx::query("UPDATE files SET description = COALESCE(description, ?) WHERE id = ?") + .bind(format!("[analysis failed: {}]", reason)) + .bind(file_id) + .execute(&self.pool) + .await?; + Ok(()) + } +} + +// Maximum number of characters from the extracted text to include in prompts. +const MAX_EXCERPT_CHARS: usize = 4000; + +async fn extract_file_excerpt(path: &str) -> Result<(String, bool)> { + let path_buf = PathBuf::from(path); + let extension = path_buf + .extension() + .and_then(|e| e.to_str()) + .map(|s| s.to_ascii_lowercase()) + .unwrap_or_default(); + + let raw_text = if extension == "pdf" { + let pdf_path = path_buf.clone(); + tokio::task::spawn_blocking(move || extract_text(&pdf_path)) + .await + .map_err(|e| anyhow!("pdf text extraction task panicked: {e}"))?? + } else { + let bytes = tokio::fs::read(&path_buf) + .await + .with_context(|| format!("reading file bytes from {path}"))?; + String::from_utf8_lossy(&bytes).into_owned() + }; + + let cleaned = raw_text.replace('\r', ""); + let condensed = collapse_whitespace(&cleaned); + let (excerpt, truncated) = truncate_to_chars(&condensed, MAX_EXCERPT_CHARS); + + Ok((excerpt, truncated)) +} + +fn truncate_to_chars(text: &str, max_chars: usize) -> (String, bool) { + if max_chars == 0 { + return (String::new(), !text.is_empty()); + } + + let mut result = String::new(); + let mut chars = text.chars(); + for _ in 0..max_chars { + match chars.next() { + Some(ch) => result.push(ch), + None => return (result, false), + } + } + + if chars.next().is_some() { + result.push('…'); + (result, true) + } else { + (result, false) + } +} + +fn collapse_whitespace(input: &str) -> String { + let mut output = String::with_capacity(input.len()); + let mut prev_was_ws = false; + for ch in input.chars() { + if ch.is_whitespace() { + if !prev_was_ws { + output.push(' '); + } + prev_was_ws = true; + } else { + prev_was_ws = false; + output.push(ch); + } + } + output.trim().to_string() +} diff --git a/rust-engine/src/gemini_client.rs b/rust-engine/src/gemini_client.rs new file mode 100644 index 0000000..a74a1a1 --- /dev/null +++ b/rust-engine/src/gemini_client.rs @@ -0,0 +1,97 @@ +use anyhow::Result; +use reqwest::Client; +use serde::Deserialize; +use serde_json::json; + +// NOTE: This file provides lightweight helpers around the Gemini API. For the +// hackathon demo we fall back to deterministic strings when the API key is not +// configured so the flows still work end-to-end. + +pub const DEMO_EMBED_DIM: usize = 64; + +/// Demo text embedding (replace with real Gemini text embedding API) +pub async fn demo_text_embedding(text: &str) -> Result> { + let mut v = vec![0f32; DEMO_EMBED_DIM]; + for (i, b) in text.as_bytes().iter().enumerate() { + let idx = i % v.len(); + v[idx] += (*b as f32) / 255.0; + } + Ok(v) +} + +/// Generate text using the default model (GEMINI_MODEL or gemini-2.5-pro). +#[allow(dead_code)] +pub async fn generate_text(prompt: &str) -> Result { + let model = std::env::var("GEMINI_MODEL").unwrap_or_else(|_| "gemini-2.5-pro".to_string()); + generate_text_with_model(&model, prompt).await +} + +/// Generate text with an explicit Gemini model. Falls back to a deterministic +/// response when the API key is not set so the demo still runs. +pub async fn generate_text_with_model(model: &str, prompt: &str) -> Result { + let api_key = match std::env::var("GEMINI_API_KEY") { + Ok(k) if !k.is_empty() => k, + _ => { + return Ok(format!( + "[demo] Gemini ({}) not configured. Prompt preview: {}", + model, + truncate(prompt, 240) + )); + } + }; + + let url = format!( + "https://generativelanguage.googleapis.com/v1beta/models/{}:generateContent?key={}", + model, api_key + ); + + let body = json!({ + "contents": [ { "parts": [ { "text": prompt } ] } ] + }); + + let client = Client::new(); + let resp = client.post(&url).json(&body).send().await?; + let status = resp.status(); + let txt = resp.text().await?; + if !status.is_success() { + return Ok(format!( + "[demo] Gemini ({}) error {}: {}", + model, + status, + truncate(&txt, 240) + )); + } + + #[derive(Deserialize)] + struct Part { + text: Option, + } + #[derive(Deserialize)] + struct Content { + parts: Vec, + } + #[derive(Deserialize)] + struct Candidate { + content: Content, + } + #[derive(Deserialize)] + struct Response { + candidates: Option>, + } + + let data: Response = serde_json::from_str(&txt).unwrap_or(Response { candidates: None }); + let out = data + .candidates + .and_then(|mut v| v.pop()) + .and_then(|c| c.content.parts.into_iter().find_map(|p| p.text)) + .unwrap_or_else(|| "[demo] Gemini returned empty response".to_string()); + Ok(out) +} + +fn truncate(s: &str, max: usize) -> String { + if s.len() <= max { + s.to_string() + } else { + format!("{}…", &s[..max]) + } +} diff --git a/rust-engine/src/main.rs b/rust-engine/src/main.rs new file mode 100644 index 0000000..006e4ce --- /dev/null +++ b/rust-engine/src/main.rs @@ -0,0 +1,87 @@ +mod api; +mod db; +mod file_worker; +mod gemini_client; +mod models; +mod storage; +mod vector; +mod vector_db; +mod worker; + +use std::env; +use std::error::Error; +use tracing::{error, info, warn}; +use warp::Filter; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Initialize tracing + tracing_subscriber::fmt::init(); + + // Load environment variables + dotenvy::dotenv().ok(); + + let database_url = env::var("DATABASE_URL") + .unwrap_or_else(|_| "mysql://astraadmin:password@mysql:3306/astra".to_string()); + + info!("Starting Rust Engine..."); + + // Ensure storage dir + storage::ensure_storage_dir().expect("storage dir"); + + // Initialize DB + let pool = db::init_db(&database_url) + .await + .map_err(|e| -> Box { Box::new(e) })?; + + let auto_import_setting = env::var("AUTO_IMPORT_DEMO").unwrap_or_else(|_| "true".to_string()); + let auto_import = !matches!( + auto_import_setting.trim().to_ascii_lowercase().as_str(), + "0" | "false" | "off" | "no" + ); + if auto_import { + match api::perform_demo_import(false, &pool).await { + Ok(summary) => { + if let Some(err_msg) = summary.error.as_ref() { + warn!(error = %err_msg, "startup demo import completed with warnings"); + } + info!( + imported = summary.imported, + skipped = summary.skipped, + files_found = summary.files_found, + source = summary.source_dir.as_deref().unwrap_or("unknown"), + "startup demo import completed" + ); + } + Err(err) => { + error!(error = %err, "startup demo import failed"); + } + } + } else { + info!("AUTO_IMPORT_DEMO disabled; skipping startup demo import"); + } + + // Spawn query worker + let worker = worker::Worker::new(pool.clone()); + tokio::spawn(async move { worker.run().await }); + + // Spawn file analysis worker + let file_worker = file_worker::FileWorker::new(pool.clone()); + tokio::spawn(async move { file_worker.run().await }); + + // API routes + let api_routes = api::routes(pool.clone()) + .with( + warp::cors() + .allow_any_origin() + .allow_headers(vec!["content-type", "authorization"]) + .allow_methods(vec!["GET", "POST", "PUT", "DELETE", "OPTIONS"]), + ) + .with(warp::log("rust_engine")); + + info!("Rust Engine started on http://0.0.0.0:8000"); + + warp::serve(api_routes).run(([0, 0, 0, 0], 8000)).await; + + Ok(()) +} diff --git a/rust-engine/src/models.rs b/rust-engine/src/models.rs new file mode 100644 index 0000000..ecea22f --- /dev/null +++ b/rust-engine/src/models.rs @@ -0,0 +1,65 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct FileRecord { + pub id: String, + pub filename: String, + pub path: String, + pub description: Option, + pub created_at: Option>, + pub pending_analysis: bool, // true if file is not yet ready for search + pub analysis_status: String, // 'Queued', 'InProgress', 'Completed', 'Failed' +} + +impl FileRecord { + #[allow(dead_code)] + pub fn new( + filename: impl Into, + path: impl Into, + description: Option, + ) -> Self { + Self { + id: Uuid::new_v4().to_string(), + filename: filename.into(), + path: path.into(), + description, + created_at: None, + pending_analysis: true, + analysis_status: "Queued".to_string(), + } + } +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub enum QueryStatus { + Queued, + InProgress, + Completed, + Cancelled, + Failed, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct QueryRecord { + pub id: String, + pub status: QueryStatus, + pub payload: serde_json::Value, + pub result: Option, + pub created_at: Option>, + pub updated_at: Option>, +} + +impl QueryRecord { + pub fn new(payload: serde_json::Value) -> Self { + Self { + id: Uuid::new_v4().to_string(), + status: QueryStatus::Queued, + payload, + result: None, + created_at: None, + updated_at: None, + } + } +} diff --git a/rust-engine/src/storage.rs b/rust-engine/src/storage.rs new file mode 100644 index 0000000..c9e9ea4 --- /dev/null +++ b/rust-engine/src/storage.rs @@ -0,0 +1,38 @@ +use anyhow::Result; +use std::fs; +use std::io::Write; +use std::path::{Path, PathBuf}; + +pub fn storage_dir() -> PathBuf { + std::env::var("ASTRA_STORAGE") + .map(PathBuf::from) + .unwrap_or_else(|_| PathBuf::from("/app/storage")) +} + +pub fn ensure_storage_dir() -> Result<()> { + let dir = storage_dir(); + if !dir.exists() { + fs::create_dir_all(&dir)?; + } + Ok(()) +} + +pub fn save_file(filename: &str, contents: &[u8]) -> Result { + ensure_storage_dir()?; + let mut path = storage_dir(); + path.push(filename); + let mut f = fs::File::create(&path)?; + f.write_all(contents)?; + Ok(path) +} + +pub fn delete_file(path: &Path) -> Result<()> { + if path.exists() { + fs::remove_file(path)?; + } + Ok(()) +} + +pub fn public_url_for(filename: &str) -> String { + format!("/storage/{}", filename) +} diff --git a/rust-engine/src/vector.rs b/rust-engine/src/vector.rs new file mode 100644 index 0000000..c34b0a3 --- /dev/null +++ b/rust-engine/src/vector.rs @@ -0,0 +1,24 @@ +use anyhow::Result; +use lazy_static::lazy_static; +use std::collections::HashMap; +use std::sync::Mutex; + +lazy_static! { + static ref VECTOR_STORE: Mutex>> = Mutex::new(HashMap::new()); +} + +pub fn store_embedding(id: &str, emb: Vec) -> Result<()> { + let mut s = VECTOR_STORE.lock().unwrap(); + s.insert(id.to_string(), emb); + Ok(()) +} + +pub fn query_top_k(_query_emb: &[f32], k: usize) -> Result> { + // Very naive: return up to k ids from the store. + let s = VECTOR_STORE.lock().unwrap(); + let mut out = Vec::new(); + for key in s.keys().take(k) { + out.push(key.clone()); + } + Ok(out) +} diff --git a/rust-engine/src/vector_db.rs b/rust-engine/src/vector_db.rs new file mode 100644 index 0000000..b8142a5 --- /dev/null +++ b/rust-engine/src/vector_db.rs @@ -0,0 +1,111 @@ +use anyhow::Result; +use reqwest::Client; +use serde::Deserialize; +use serde_json::json; + +#[derive(Clone)] +pub struct QdrantClient { + base: String, + client: Client, +} + +impl QdrantClient { + /// Delete a point from collection 'files' by id + pub async fn delete_point(&self, id: &str) -> Result<()> { + let url = format!("{}/collections/files/points/delete", self.base); + let body = json!({ + "points": [id] + }); + let resp = self.client.post(&url).json(&body).send().await?; + let status = resp.status(); + if status.is_success() { + Ok(()) + } else { + let t = resp.text().await.unwrap_or_default(); + Err(anyhow::anyhow!("qdrant delete failed: {} - {}", status, t)) + } + } + pub fn new(base: &str) -> Self { + Self { + base: base.trim_end_matches('/').to_string(), + client: Client::new(), + } + } + + /// Upsert a point into collection `files` with id and vector + pub async fn upsert_point(&self, id: &str, vector: Vec) -> Result<()> { + let url = format!("{}/collections/files/points", self.base); + let body = json!({ + "points": [{ + "id": id, + "vector": vector, + "payload": {"type": "file"} + }] + }); + + let resp = self.client.post(&url).json(&body).send().await?; + let status = resp.status(); + if status.is_success() { + Ok(()) + } else { + let t = resp.text().await.unwrap_or_default(); + Err(anyhow::anyhow!("qdrant upsert failed: {} - {}", status, t)) + } + } + + /// Ensure the 'files' collection exists with the given dimension and distance metric + pub async fn ensure_files_collection(&self, dim: usize) -> Result<()> { + let url = format!("{}/collections/files", self.base); + let body = json!({ + "vectors": {"size": dim, "distance": "Cosine"} + }); + let resp = self.client.put(&url).json(&body).send().await?; + // 200 OK or 201 Created means ready; 409 Conflict means already exists + if resp.status().is_success() || resp.status().as_u16() == 409 { + Ok(()) + } else { + let status = resp.status(); + let t = resp.text().await.unwrap_or_default(); + Err(anyhow::anyhow!( + "qdrant ensure collection failed: {} - {}", + status, + t + )) + } + } + + /// Search top-k nearest points from 'files', return (id, score) + pub async fn search_top_k(&self, vector: Vec, k: usize) -> Result> { + let url = format!("{}/collections/files/points/search", self.base); + let body = json!({ + "vector": vector, + "limit": k + }); + let resp = self.client.post(&url).json(&body).send().await?; + let status = resp.status(); + if !status.is_success() { + let t = resp.text().await.unwrap_or_default(); + return Err(anyhow::anyhow!("qdrant search failed: {} - {}", status, t)); + } + #[derive(Deserialize)] + struct Hit { + id: serde_json::Value, + score: f32, + } + #[derive(Deserialize)] + struct Data { + result: Vec, + } + let data: Data = resp.json().await?; + let mut out = Vec::new(); + for h in data.result { + // id can be string or number; handle string + if let Some(s) = h.id.as_str() { + out.push((s.to_string(), h.score)); + } else { + out.push((h.id.to_string(), h.score)); + } + } + Ok(out) + } +} diff --git a/rust-engine/src/worker.rs b/rust-engine/src/worker.rs new file mode 100644 index 0000000..68b8faa --- /dev/null +++ b/rust-engine/src/worker.rs @@ -0,0 +1,298 @@ +use crate::gemini_client::{demo_text_embedding, generate_text_with_model, DEMO_EMBED_DIM}; +use crate::models::{QueryRecord, QueryStatus}; +use crate::storage; +use crate::vector; +use crate::vector_db::QdrantClient; +use anyhow::Result; +use sqlx::MySqlPool; +use std::time::Duration; +use tracing::{error, info}; + +pub struct Worker { + pool: MySqlPool, + qdrant: QdrantClient, +} + +impl Worker { + pub fn new(pool: MySqlPool) -> Self { + let qdrant_url = + std::env::var("QDRANT_URL").unwrap_or_else(|_| "http://qdrant:6333".to_string()); + let qdrant = QdrantClient::new(&qdrant_url); + Self { pool, qdrant } + } + + pub async fn run(&self) { + info!("Worker starting"); + + // Ensure qdrant collection exists + if let Err(e) = self.qdrant.ensure_files_collection(DEMO_EMBED_DIM).await { + error!("Failed to ensure Qdrant collection: {}", e); + } + + // Requeue stale InProgress jobs older than cutoff (e.g., 10 minutes) + if let Err(e) = self.requeue_stale_inprogress(10 * 60).await { + error!("Failed to requeue stale jobs: {}", e); + } + + loop { + // Claim next queued query + match self.fetch_and_claim().await { + Ok(Some(mut q)) => { + info!("Processing query {}", q.id); + if let Err(e) = self.process_query(&mut q).await { + error!("Error processing {}: {}", q.id, e); + let _ = self.mark_failed(&q.id, &format!("{}", e)).await; + } + } + Ok(None) => { + tokio::time::sleep(Duration::from_secs(2)).await; + } + Err(e) => { + error!("Worker fetch error: {}", e); + tokio::time::sleep(Duration::from_secs(5)).await; + } + } + } + } + + async fn fetch_and_claim(&self) -> Result> { + // Note: MySQL transactional SELECT FOR UPDATE handling is more complex; for this hackathon scaffold + // we do a simple two-step: select one queued id, then update it to InProgress if it is still queued. + if let Some(row) = sqlx::query( + "SELECT id, payload FROM queries WHERE status = 'Queued' ORDER BY created_at LIMIT 1", + ) + .fetch_optional(&self.pool) + .await? + { + use sqlx::Row; + let id: String = row.get("id"); + let payload: serde_json::Value = row.get("payload"); + + let updated = sqlx::query( + "UPDATE queries SET status = 'InProgress' WHERE id = ? AND status = 'Queued'", + ) + .bind(&id) + .execute(&self.pool) + .await?; + + if updated.rows_affected() == 1 { + let mut q = QueryRecord::new(payload); + q.id = id; + q.status = QueryStatus::InProgress; + return Ok(Some(q)); + } + } + Ok(None) + } + + async fn process_query(&self, q: &mut QueryRecord) -> Result<()> { + // Stage 1: set InProgress (idempotent) + self.update_status(&q.id, QueryStatus::InProgress).await?; + + // Stage 2: embed query text + let text = q.payload.get("q").and_then(|v| v.as_str()).unwrap_or(""); + let emb = demo_text_embedding(text).await?; + let top_k = q.payload.get("top_k").and_then(|v| v.as_u64()).unwrap_or(5) as usize; + let top_k = top_k.max(1).min(20); + + // Check cancellation + if self.is_cancelled(&q.id).await? { + return Ok(()); + } + + // Stage 3: search top-K in Qdrant + let hits = match self.qdrant.search_top_k(emb.clone(), top_k).await { + Ok(list) if !list.is_empty() => list, + Ok(_) => Vec::new(), + Err(err) => { + error!("Qdrant search failed for query {}: {}", q.id, err); + Vec::new() + } + }; + + let hits = if hits.is_empty() { + match vector::query_top_k(&emb, top_k) { + Ok(fallback_ids) if !fallback_ids.is_empty() => { + info!("Using in-memory fallback for query {}", q.id); + fallback_ids.into_iter().map(|id| (id, 0.0)).collect() + } + _ => Vec::new(), + } + } else { + hits + }; + + // Check cancellation + if self.is_cancelled(&q.id).await? { + return Ok(()); + } + + // Stage 4: fetch file metadata for IDs + let mut files_json = Vec::new(); + for (fid, score) in hits { + if let Some(row) = sqlx::query("SELECT id, filename, path, description, analysis_status FROM files WHERE id = ? AND pending_analysis = FALSE") + .bind(&fid) + .fetch_optional(&self.pool) + .await? { + use sqlx::Row; + let id: String = row.get("id"); + let filename: String = row.get("filename"); + let path: String = row.get("path"); + let description: Option = row.get("description"); + let status: Option = row.try_get("analysis_status").ok(); + let storage_url = storage::public_url_for(&filename); + files_json.push(serde_json::json!({ + "id": id, + "filename": filename, + "path": path, + "storage_url": storage_url, + "description": description, + "analysis_status": status, + "score": score + })); + } + } + + // Stage 5: call Gemini to analyze relationships and propose follow-up details strictly from provided files + let relationships_prompt = build_relationships_prompt(text, &files_json); + let (relationships, final_answer) = if files_json.is_empty() { + ( + "No analyzed files are ready yet. Try seeding demo data or wait for processing to finish.".to_string(), + "I could not find any relevant documents yet. Once files finish analysis I will be able to answer.".to_string(), + ) + } else { + let relationships = generate_text_with_model("gemini-2.5-pro", &relationships_prompt) + .await + .unwrap_or_else(|e| format!("[demo] relationships error: {}", e)); + + // Stage 6: final answer synthesis with strict constraints (no speculation; say unknown when insufficient) + let final_prompt = build_final_answer_prompt(text, &files_json, &relationships); + let final_answer = generate_text_with_model("gemini-2.5-pro", &final_prompt) + .await + .unwrap_or_else(|e| format!("[demo] final answer error: {}", e)); + (relationships, final_answer) + }; + + // Stage 7: persist results + let result = serde_json::json!({ + "summary": format!("Found {} related files", files_json.len()), + "related_files": files_json, + "relationships": relationships, + "final_answer": final_answer, + }); + sqlx::query("UPDATE queries SET status = 'Completed', result = ? WHERE id = ?") + .bind(result) + .bind(&q.id) + .execute(&self.pool) + .await?; + Ok(()) + } + + async fn update_status(&self, id: &str, status: QueryStatus) -> Result<()> { + let s = match status { + QueryStatus::Queued => "Queued", + QueryStatus::InProgress => "InProgress", + QueryStatus::Completed => "Completed", + QueryStatus::Cancelled => "Cancelled", + QueryStatus::Failed => "Failed", + }; + sqlx::query("UPDATE queries SET status = ? WHERE id = ?") + .bind(s) + .bind(id) + .execute(&self.pool) + .await?; + Ok(()) + } + + async fn mark_failed(&self, id: &str, message: &str) -> Result<()> { + let result = serde_json::json!({"error": message}); + sqlx::query("UPDATE queries SET status = 'Failed', result = ? WHERE id = ?") + .bind(result) + .bind(id) + .execute(&self.pool) + .await?; + Ok(()) + } + + async fn requeue_stale_inprogress(&self, age_secs: i64) -> Result<()> { + // MySQL: requeue items updated_at < now()-age and status = InProgress + sqlx::query( + "UPDATE queries SET status = 'Queued' WHERE status = 'InProgress' AND updated_at < (NOW() - INTERVAL ? SECOND)" + ) + .bind(age_secs) + .execute(&self.pool) + .await?; + Ok(()) + } + + async fn is_cancelled(&self, id: &str) -> Result { + if let Some(row) = sqlx::query("SELECT status FROM queries WHERE id = ?") + .bind(id) + .fetch_optional(&self.pool) + .await? + { + use sqlx::Row; + let s: String = row.get("status"); + return Ok(s == "Cancelled"); + } + Ok(false) + } +} + +fn build_relationships_prompt(query: &str, files: &Vec) -> String { + let files_snippets: Vec = files + .iter() + .map(|f| { + format!( + "- id: {id}, filename: {name}, path: {path}, desc: {desc}", + id = f.get("id").and_then(|v| v.as_str()).unwrap_or(""), + name = f.get("filename").and_then(|v| v.as_str()).unwrap_or(""), + path = f.get("path").and_then(|v| v.as_str()).unwrap_or(""), + desc = f.get("description").and_then(|v| v.as_str()).unwrap_or("") + ) + }) + .collect(); + format!( + "You are an assistant analyzing relationships STRICTLY within the provided files.\n\ + Query: {query}\n\ + Files:\n{files}\n\ + Tasks:\n\ + 1) Summarize key details from the files relevant to the query.\n\ + 2) Describe relationships and linkages strictly supported by these files.\n\ + 3) List important follow-up questions that could be answered only using the provided files.\n\ + Rules: Do NOT guess or invent. If information is insufficient in the files, explicitly state that.", + query=query, + files=files_snippets.join("\n") + ) +} + +fn build_final_answer_prompt( + query: &str, + files: &Vec, + relationships: &str, +) -> String { + let files_short: Vec = files + .iter() + .map(|f| { + format!( + "- {name} ({id})", + id = f.get("id").and_then(|v| v.as_str()).unwrap_or(""), + name = f.get("filename").and_then(|v| v.as_str()).unwrap_or("") + ) + }) + .collect(); + format!( + "You are to compose a final answer to the user query using only the information from the files.\n\ + Query: {query}\n\ + Files considered:\n{files}\n\ + Relationship analysis:\n{rels}\n\ + Requirements:\n\ + - Use only information present in the files and analysis above.\n\ + - If the answer is uncertain or cannot be determined from the files, clearly state that limitation.\n\ + - Avoid speculation or assumptions.\n\ + Provide a concise, structured answer.", + query=query, + files=files_short.join("\n"), + rels=relationships + ) +} diff --git a/src/features/gemini/gemini.js b/src/features/gemini/gemini.js new file mode 100644 index 0000000..60ab8c1 --- /dev/null +++ b/src/features/gemini/gemini.js @@ -0,0 +1,4 @@ +import { createPartFromUri, GoogleGenAI } from "@google/genai" +import 'dotenv/config' + +const ai = new GoogleGenAI({ apiKey: ${} }) \ No newline at end of file diff --git a/frontend/Dockerfile b/web-app/Dockerfile similarity index 100% rename from frontend/Dockerfile rename to web-app/Dockerfile diff --git a/web-app/README.md b/web-app/README.md new file mode 100644 index 0000000..18bc70e --- /dev/null +++ b/web-app/README.md @@ -0,0 +1,16 @@ +# React + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. diff --git a/web-app/components.json b/web-app/components.json new file mode 100644 index 0000000..7a2e4d8 --- /dev/null +++ b/web-app/components.json @@ -0,0 +1,15 @@ +{ + "style": "default", + "tailwind": { + "config": "tailwind.config.js", + "css": "src/app/globals.css", + "baseColor": "zinc", + "cssVariables": true + }, + "rsc": false, + "tsx": false, + "aliases": { + "utils": "~/lib/utils", + "components": "~/components" + } +} diff --git a/frontend/eslint.config.js b/web-app/eslint.config.js similarity index 51% rename from frontend/eslint.config.js rename to web-app/eslint.config.js index 050e89d..53a87c7 100644 --- a/frontend/eslint.config.js +++ b/web-app/eslint.config.js @@ -1,15 +1,18 @@ import js from "@eslint/js"; import globals from "globals"; -import reactHooks from "eslint-plugin-react-hooks"; +import reactHooks from "eslint-plugin-react-hooks"; // Or import { configs as reactHooks } from "eslint-plugin-react-hooks"; import reactRefresh from "eslint-plugin-react-refresh"; +import { defineConfig, globalIgnores } from "eslint/config"; -export default [ +export default defineConfig([ + globalIgnores(["dist"]), { - ignores: ["dist/**"], - }, - js.configs.recommended, - { - files: ["**/*.{js,jsx}"], + files: ["**/*{js,jsx}"], + extends: [ + js.configs.recommended, + reactHooks.configs["recommended-latest"], + reactRefresh.configs.vite, + ], languageOptions: { ecmaVersion: 2020, globals: globals.browser, @@ -19,17 +22,8 @@ export default [ sourceType: "module", }, }, - plugins: { - "react-hooks": reactHooks, - "react-refresh": reactRefresh, - }, rules: { - ...reactHooks.configs.recommended.rules, - "react-refresh/only-export-components": [ - "warn", - { allowConstantExport: true }, - ], "no-unused-vars": ["error", { varsIgnorePattern: "^[A-Z_]" }], }, }, -]; +]); diff --git a/frontend/index.html b/web-app/index.html similarity index 100% rename from frontend/index.html rename to web-app/index.html diff --git a/web-app/jsconfig.json b/web-app/jsconfig.json new file mode 100644 index 0000000..604c23f --- /dev/null +++ b/web-app/jsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "baseUrl": "./", + "lib": ["es2015", "dom"] + }, + "include": ["src"] +} diff --git a/frontend/package-lock.json b/web-app/package-lock.json similarity index 99% rename from frontend/package-lock.json rename to web-app/package-lock.json index 7dca15a..aa53229 100644 --- a/frontend/package-lock.json +++ b/web-app/package-lock.json @@ -33,7 +33,6 @@ }, "devDependencies": { "@eslint/js": "^9.38.0", - "daisyui": "^5.4.7", "eslint": "^9.38.0", "eslint-plugin-import": "^2.32.0", "eslint-plugin-react": "^7.37.5", @@ -2907,16 +2906,6 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, - "node_modules/daisyui": { - "version": "5.4.7", - "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.4.7.tgz", - "integrity": "sha512-2wYO61vTPCXk7xEBgnzLZAYoE0xS5IRLu/GSq0vORpB+cTrtubdx69NnA0loc0exvCY1s2fYL4lGZtFHe2ohNQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/saadeghi/daisyui?sponsor=1" - } - }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", diff --git a/frontend/package.json b/web-app/package.json similarity index 94% rename from frontend/package.json rename to web-app/package.json index b1476ae..5b7412b 100644 --- a/frontend/package.json +++ b/web-app/package.json @@ -15,8 +15,8 @@ "dependencies": { "@google/genai": "^1.25.0", "@tailwindcss/postcss": "^4.1.14", - "@tailwindcss/vite": "^4.1.14", - "@vitejs/plugin-react-swc": "^3.7.0", + "@tailwindcss/vite": "^4.1.14", + "@vitejs/plugin-react-swc": "^3.7.0", "bootstrap": "^5.3.8", "bootstrap-icons": "^1.13.1", "class-variance-authority": "^0.7.1", @@ -40,7 +40,7 @@ "packageManager": ">=npm@10.9.0", "devDependencies": { "@eslint/js": "^9.38.0", - "daisyui": "^5.4.7", + "daisyui": "^5.3.7", "eslint": "^9.38.0", "eslint-plugin-import": "^2.32.0", "eslint-plugin-react": "^7.37.5", diff --git a/web-app/public/pdfs/SRHB.pdf b/web-app/public/pdfs/SRHB.pdf new file mode 100644 index 0000000..5bb4f78 Binary files /dev/null and b/web-app/public/pdfs/SRHB.pdf differ diff --git a/web-app/public/pdfs/falcon-users-guide-2025-05-09.pdf b/web-app/public/pdfs/falcon-users-guide-2025-05-09.pdf new file mode 100644 index 0000000..4d835a6 Binary files /dev/null and b/web-app/public/pdfs/falcon-users-guide-2025-05-09.pdf differ diff --git a/web-app/public/pdfs/spacex-falcon-9-data-sheet.pdf b/web-app/public/pdfs/spacex-falcon-9-data-sheet.pdf new file mode 100644 index 0000000..d6ce2ae Binary files /dev/null and b/web-app/public/pdfs/spacex-falcon-9-data-sheet.pdf differ diff --git a/web-app/public/pdfs/system-safety-handbook.pdf b/web-app/public/pdfs/system-safety-handbook.pdf new file mode 100644 index 0000000..7d076f2 Binary files /dev/null and b/web-app/public/pdfs/system-safety-handbook.pdf differ diff --git a/web-app/server.mjs b/web-app/server.mjs new file mode 100644 index 0000000..cb08374 --- /dev/null +++ b/web-app/server.mjs @@ -0,0 +1,80 @@ +import express from 'express'; +import path from 'node:path'; +import helmet from 'helmet'; +import cors from 'cors'; +import http from 'node:http'; +import https from 'node:https'; +import { URL } from 'node:url'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const app = express(); +const PORT = Number(process.env.PORT) || 3000; +const HOST = process.env.HOST || '0.0.0.0'; +const RUST_ENGINE_BASE = + process.env.RUST_ENGINE_BASE || + process.env.RUST_ENGINE_URL || + 'http://rust-engine:8000'; +const STORAGE_DIR = path.resolve(process.env.ASTRA_STORAGE || '/app/storage'); + +app.set('trust proxy', true); +app.use(helmet({ contentSecurityPolicy: false })); +app.use(cors()); +app.get('/api/healthz', (_req, res) => { + res.json({ status: 'ok', upstream: RUST_ENGINE_BASE }); +}); + +// Proxy all /api/* calls (including POST bodies, multipart uploads, etc.) +app.use('/api', (req, res) => { + const targetUrl = new URL(req.originalUrl, RUST_ENGINE_BASE); + const client = targetUrl.protocol === 'https:' ? https : http; + + const headers = { ...req.headers, host: targetUrl.host }; + + const proxyReq = client.request( + targetUrl, + { + method: req.method, + headers, + }, + (upstream) => { + res.status(upstream.statusCode || 502); + for (const [key, value] of Object.entries(upstream.headers)) { + if (typeof value !== 'undefined') { + res.setHeader(key, value); + } + } + upstream.pipe(res); + } + ); + + proxyReq.on('error', (err) => { + console.error('API proxy error:', err); + if (!res.headersSent) { + res.status(502).json({ error: 'proxy_failed' }); + } else { + res.end(); + } + }); + + req.pipe(proxyReq); +}); + +// Serve static frontend +const distDir = path.resolve(__dirname, 'dist'); +app.use(express.static(distDir)); + +// Expose imported files for the UI (read-only) +app.use('/storage', express.static(STORAGE_DIR)); + +// SPA fallback (Express 5 requires middleware instead of bare '*') +app.use((req, res) => { + res.sendFile(path.join(distDir, 'index.html')); +}); + +app.listen(PORT, HOST, () => { + console.log(`Web app server listening on http://${HOST}:${PORT}`); + console.log(`Proxying to rust engine at ${RUST_ENGINE_BASE}`); +}); diff --git a/frontend/src/app/index.jsx b/web-app/src/app/index.jsx similarity index 100% rename from frontend/src/app/index.jsx rename to web-app/src/app/index.jsx diff --git a/frontend/src/components/layouts/chat-layout.jsx b/web-app/src/components/layouts/chat-layout.jsx similarity index 94% rename from frontend/src/components/layouts/chat-layout.jsx rename to web-app/src/components/layouts/chat-layout.jsx index 951d54c..bbbdbdf 100644 --- a/frontend/src/components/layouts/chat-layout.jsx +++ b/web-app/src/components/layouts/chat-layout.jsx @@ -1,10 +1,4 @@ -import React, { - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from "react"; +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import ChatHeader from "src/components/ui/chat/chat-header"; import ChatWindow from "src/components/ui/chat/chat-window"; import MessageInput from "src/components/ui/chat/message-input"; @@ -16,7 +10,7 @@ import { } from "src/lib/api"; const createId = () => - globalThis.crypto?.randomUUID?.() ?? `id-${Date.now()}-${Math.random()}`; + (globalThis.crypto?.randomUUID?.() ?? `id-${Date.now()}-${Math.random()}`); const INTRO_MESSAGE = { id: "intro", @@ -182,8 +176,8 @@ export default function ChatLayout() { prev.map((message) => message.id === placeholderId ? { ...message, content, pending: false } - : message, - ), + : message + ) ); } catch (error) { const message = error?.message || "Something went wrong."; @@ -191,13 +185,13 @@ export default function ChatLayout() { prev.map((entry) => entry.id === placeholderId ? { - ...entry, - content: `⚠️ ${message}`, - pending: false, - error: true, - } - : entry, - ), + ...entry, + content: `⚠️ ${message}`, + pending: false, + error: true, + } + : entry + ) ); showError(message); } finally { @@ -212,7 +206,7 @@ export default function ChatLayout() { refreshFiles, waitForResult, buildAssistantMarkdown, - ], + ] ); const handleDeleteAll = useCallback(() => { diff --git a/frontend/src/components/ui/button/delete-button.jsx b/web-app/src/components/ui/button/delete-button.jsx similarity index 100% rename from frontend/src/components/ui/button/delete-button.jsx rename to web-app/src/components/ui/button/delete-button.jsx diff --git a/frontend/src/components/ui/button/down-button.jsx b/web-app/src/components/ui/button/down-button.jsx similarity index 54% rename from frontend/src/components/ui/button/down-button.jsx rename to web-app/src/components/ui/button/down-button.jsx index b55558b..b51dc3f 100644 --- a/frontend/src/components/ui/button/down-button.jsx +++ b/web-app/src/components/ui/button/down-button.jsx @@ -3,17 +3,9 @@ import { ArrowDown } from "lucide-react"; import { motion } from "motion/react"; export default function DownButton({ onClick }) { - function handleClick(e) { - if (onClick) return onClick(e); - // default behavior: scroll to bottom of page smoothly - const doc = document.documentElement; - const top = Math.max(doc.scrollHeight, document.body.scrollHeight); - window.scrollTo({ top, behavior: "smooth" }); - } - return ( [...s, ...files]); + if (onFiles) onFiles(files); + if (inputRef.current) inputRef.current.value = null; + } + + function removeFile(index) { + setFilesList((s) => { + const copy = [...s]; + copy.splice(index, 1); + return copy; + }); + } + + return ( +
+ + + {filesList.length > 0 && ( +
+ {filesList.map((f, i) => ( +
+ {f.name} + +
+ ))} +
+ )} +
+ ); +} diff --git a/frontend/src/components/ui/chat/chat-header.jsx b/web-app/src/components/ui/chat/chat-header.jsx similarity index 97% rename from frontend/src/components/ui/chat/chat-header.jsx rename to web-app/src/components/ui/chat/chat-header.jsx index b5d9a63..7e0a49e 100644 --- a/frontend/src/components/ui/chat/chat-header.jsx +++ b/web-app/src/components/ui/chat/chat-header.jsx @@ -2,7 +2,7 @@ import React, { useEffect, useMemo, useState } from "react"; import { motion } from "motion/react"; import { Rocket } from "lucide-react"; import DeleteButton from "src/components/ui/button/delete-button"; -import SchematicButton from "src/components/ui/button/schematic-button"; +import SchematicButton from "../button/schematic-button"; export default function ChatHeader({ title = "Title of Chat", diff --git a/frontend/src/components/ui/chat/chat-window.jsx b/web-app/src/components/ui/chat/chat-window.jsx similarity index 100% rename from frontend/src/components/ui/chat/chat-window.jsx rename to web-app/src/components/ui/chat/chat-window.jsx diff --git a/frontend/src/components/ui/chat/message-input.jsx b/web-app/src/components/ui/chat/message-input.jsx similarity index 55% rename from frontend/src/components/ui/chat/message-input.jsx rename to web-app/src/components/ui/chat/message-input.jsx index 0f49a7f..07751a3 100644 --- a/frontend/src/components/ui/chat/message-input.jsx +++ b/web-app/src/components/ui/chat/message-input.jsx @@ -12,52 +12,10 @@ export default function MessageInput({ onSend, disabled = false }) { if (textareaRef.current) textareaRef.current.style.height = "auto"; }, []); - async function handleSubmit(e) { + function handleSubmit(e) { e.preventDefault(); if (!text.trim() || disabled) return; onSend(text.trim()); - - // create query on backend - try { - if (onMessage) - onMessage("assistant", "Queued: sending request to server..."); - const createRes = await fetch(`/api/query/create`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ q: text, top_k: 5 }), - }); - const createJson = await createRes.json(); - const id = createJson.id; - if (!id) throw new Error("no id returned"); - - // poll status - let status = "Queued"; - if (onMessage) onMessage("assistant", `Status: ${status}`); - while (status !== "Completed" && status !== "Failed") { - await new Promise((r) => setTimeout(r, 1000)); - const sRes = await fetch(`/api/query/status?id=${id}`); - const sJson = await sRes.json(); - status = sJson.status; - if (onMessage) onMessage("assistant", `Status: ${status}`); - if (status === "Cancelled") break; - } - - if (status === "Completed") { - const resultRes = await fetch(`/api/query/result?id=${id}`); - const resultJson = await resultRes.json(); - const final = - resultJson?.result?.final_answer || - JSON.stringify(resultJson?.result || {}); - if (onMessage) onMessage("assistant", final); - } else { - if (onMessage) - onMessage("assistant", `Query status ended as: ${status}`); - } - } catch (err) { - console.error(err); - if (onMessage) onMessage("assistant", `Error: ${err.message}`); - } - setText(""); } @@ -65,8 +23,10 @@ export default function MessageInput({ onSend, disabled = false }) {