Merge branch 'rust-dev'
This commit is contained in:
commit
de632411a8
28 changed files with 232 additions and 7 deletions
48
rust-engine/DEMODETAILS.md
Normal file
48
rust-engine/DEMODETAILS.md
Normal file
|
|
@ -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
|
||||||
|
|
@ -66,6 +66,14 @@
|
||||||
1. docker compose up -d mysql qdrant
|
1. docker compose up -d mysql qdrant
|
||||||
2. set env DATABASE_URL and QDRANT_URL
|
2. set env DATABASE_URL and QDRANT_URL
|
||||||
3. cargo run
|
3. cargo run
|
||||||
|
4. (optional) import demo PDFs
|
||||||
|
- Ensure demo files are located in `rust-engine/demo-data` (default) or set `DEMO_DATA_DIR` env var to a folder containing PDFs.
|
||||||
|
- Call the endpoint:
|
||||||
|
- POST <http://localhost:8000/api/files/import-demo>
|
||||||
|
- Optional query `?force=1` to overwrite existing by filename
|
||||||
|
- Or run the PowerShell helper:
|
||||||
|
- `./scripts/import_demo.ps1` (adds all PDFs in demo-data)
|
||||||
|
- `./scripts/import_demo.ps1 -Force` (overwrite existing)
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
|
|
|
||||||
BIN
rust-engine/demo-data/132-1-Final.pdf
Normal file
BIN
rust-engine/demo-data/132-1-Final.pdf
Normal file
Binary file not shown.
BIN
rust-engine/demo-data/179225main_iss_poster_back.pdf
Normal file
BIN
rust-engine/demo-data/179225main_iss_poster_back.pdf
Normal file
Binary file not shown.
BIN
rust-engine/demo-data/19790004570.pdf
Normal file
BIN
rust-engine/demo-data/19790004570.pdf
Normal file
Binary file not shown.
BIN
rust-engine/demo-data/19880012104.pdf
Normal file
BIN
rust-engine/demo-data/19880012104.pdf
Normal file
Binary file not shown.
BIN
rust-engine/demo-data/19890016674.pdf
Normal file
BIN
rust-engine/demo-data/19890016674.pdf
Normal file
Binary file not shown.
BIN
rust-engine/demo-data/19920015843.pdf
Normal file
BIN
rust-engine/demo-data/19920015843.pdf
Normal file
Binary file not shown.
BIN
rust-engine/demo-data/19950014639.pdf
Normal file
BIN
rust-engine/demo-data/19950014639.pdf
Normal file
Binary file not shown.
BIN
rust-engine/demo-data/20040171627.pdf
Normal file
BIN
rust-engine/demo-data/20040171627.pdf
Normal file
Binary file not shown.
BIN
rust-engine/demo-data/20050207388.pdf
Normal file
BIN
rust-engine/demo-data/20050207388.pdf
Normal file
Binary file not shown.
BIN
rust-engine/demo-data/20050210002.pdf
Normal file
BIN
rust-engine/demo-data/20050210002.pdf
Normal file
Binary file not shown.
BIN
rust-engine/demo-data/20080014096.pdf
Normal file
BIN
rust-engine/demo-data/20080014096.pdf
Normal file
Binary file not shown.
BIN
rust-engine/demo-data/20100029672.pdf
Normal file
BIN
rust-engine/demo-data/20100029672.pdf
Normal file
Binary file not shown.
BIN
rust-engine/demo-data/20110014867.pdf
Normal file
BIN
rust-engine/demo-data/20110014867.pdf
Normal file
Binary file not shown.
BIN
rust-engine/demo-data/20120002931.pdf
Normal file
BIN
rust-engine/demo-data/20120002931.pdf
Normal file
Binary file not shown.
BIN
rust-engine/demo-data/20190028718.pdf
Normal file
BIN
rust-engine/demo-data/20190028718.pdf
Normal file
Binary file not shown.
BIN
rust-engine/demo-data/20200003149.pdf
Normal file
BIN
rust-engine/demo-data/20200003149.pdf
Normal file
Binary file not shown.
BIN
rust-engine/demo-data/473486main_iss_atcs_overview.pdf
Normal file
BIN
rust-engine/demo-data/473486main_iss_atcs_overview.pdf
Normal file
Binary file not shown.
BIN
rust-engine/demo-data/8Mod6Prob1.pdf
Normal file
BIN
rust-engine/demo-data/8Mod6Prob1.pdf
Normal file
Binary file not shown.
BIN
rust-engine/demo-data/ICES_2023_311 final 5 15 23.pdf
Normal file
BIN
rust-engine/demo-data/ICES_2023_311 final 5 15 23.pdf
Normal file
Binary file not shown.
Binary file not shown.
0
rust-engine/scripts/import_demo.ps1
Normal file
0
rust-engine/scripts/import_demo.ps1
Normal file
|
|
@ -16,6 +16,17 @@ struct DeleteQuery {
|
||||||
pub fn routes(pool: MySqlPool) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
|
pub fn routes(pool: MySqlPool) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
|
||||||
let pool_filter = warp::any().map(move || pool.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::<std::collections::HashMap<String, String>>()
|
||||||
|
.or(warp::any().map(|| std::collections::HashMap::new()))
|
||||||
|
.unify()
|
||||||
|
)
|
||||||
|
.and(pool_filter.clone())
|
||||||
|
.and_then(handle_import_demo);
|
||||||
|
|
||||||
// Upload file
|
// Upload file
|
||||||
let upload = warp::path("files")
|
let upload = warp::path("files")
|
||||||
.and(warp::post())
|
.and(warp::post())
|
||||||
|
|
@ -64,7 +75,7 @@ pub fn routes(pool: MySqlPool) -> impl Filter<Extract = impl Reply, Error = Reje
|
||||||
.and(pool_filter.clone())
|
.and(pool_filter.clone())
|
||||||
.and_then(handle_cancel_query);
|
.and_then(handle_cancel_query);
|
||||||
|
|
||||||
let api = upload.or(delete).or(list).or(create_q).or(status).or(result).or(cancel);
|
let api = upload.or(import_demo).or(delete).or(list).or(create_q).or(status).or(result).or(cancel);
|
||||||
warp::path("api").and(api)
|
warp::path("api").and(api)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -123,6 +134,70 @@ async fn handle_upload(mut form: FormData, pool: MySqlPool) -> Result<impl Reply
|
||||||
Ok(warp::reply::json(&serde_json::json!({"success": true})))
|
Ok(warp::reply::json(&serde_json::json!({"success": true})))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn handle_import_demo(params: std::collections::HashMap<String, String>, pool: MySqlPool) -> Result<impl Reply, Rejection> {
|
||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
let force = params.get("force").map(|v| v == "1" || v.eq_ignore_ascii_case("true")).unwrap_or(false);
|
||||||
|
let demo_dir = std::env::var("DEMO_DATA_DIR").unwrap_or_else(|_| "demo-data".to_string());
|
||||||
|
let base = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
|
||||||
|
let src_dir = base.join(demo_dir);
|
||||||
|
if !src_dir.exists() {
|
||||||
|
return Ok(warp::reply::json(&serde_json::json!({
|
||||||
|
"imported": 0,
|
||||||
|
"skipped": 0,
|
||||||
|
"error": format!("demo dir not found: {}", src_dir.display())
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
let mut imported = 0;
|
||||||
|
let mut skipped = 0;
|
||||||
|
for entry in fs::read_dir(&src_dir).map_err(|_| warp::reject())? {
|
||||||
|
let entry = entry.map_err(|_| warp::reject())?;
|
||||||
|
let path = entry.path();
|
||||||
|
if path.extension().and_then(|e| e.to_str()).map(|e| e.eq_ignore_ascii_case("pdf")).unwrap_or(false) {
|
||||||
|
let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("unknown.pdf").to_string();
|
||||||
|
|
||||||
|
// check if exists
|
||||||
|
if !force {
|
||||||
|
if let Some(_) = sqlx::query("SELECT id FROM files WHERE filename = ?")
|
||||||
|
.bind(&filename)
|
||||||
|
.fetch_optional(&pool)
|
||||||
|
.await
|
||||||
|
.map_err(|_| warp::reject())? {
|
||||||
|
skipped += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// read and save to storage
|
||||||
|
let data = fs::read(&path).map_err(|_| warp::reject())?;
|
||||||
|
let stored_path = storage::save_file(&filename, &data).map_err(|_| warp::reject())?;
|
||||||
|
|
||||||
|
// insert or upsert db record
|
||||||
|
let id = uuid::Uuid::new_v4().to_string();
|
||||||
|
if force {
|
||||||
|
let _ = sqlx::query("DELETE FROM files WHERE filename = ?")
|
||||||
|
.bind(&filename)
|
||||||
|
.execute(&pool)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
sqlx::query("INSERT INTO files (id, filename, path, description, pending_analysis, analysis_status) VALUES (?, ?, ?, ?, ?, 'Queued')")
|
||||||
|
.bind(&id)
|
||||||
|
.bind(&filename)
|
||||||
|
.bind(stored_path.to_str().unwrap())
|
||||||
|
.bind(Option::<String>::None)
|
||||||
|
.bind(true)
|
||||||
|
.execute(&pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("DB insert error: {}", e);
|
||||||
|
warp::reject()
|
||||||
|
})?;
|
||||||
|
imported += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(warp::reply::json(&serde_json::json!({ "imported": imported, "skipped": skipped })))
|
||||||
|
}
|
||||||
|
|
||||||
async fn handle_delete(q: DeleteQuery, pool: MySqlPool) -> Result<impl Reply, Rejection> {
|
async fn handle_delete(q: DeleteQuery, pool: MySqlPool) -> Result<impl Reply, Rejection> {
|
||||||
if let Some(row) = sqlx::query("SELECT path FROM files WHERE id = ?")
|
if let Some(row) = sqlx::query("SELECT path FROM files WHERE id = ?")
|
||||||
.bind(&q.id)
|
.bind(&q.id)
|
||||||
|
|
@ -143,7 +218,7 @@ async fn handle_delete(q: DeleteQuery, pool: MySqlPool) -> Result<impl Reply, Re
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_list(pool: MySqlPool) -> Result<impl Reply, Rejection> {
|
async fn handle_list(pool: MySqlPool) -> Result<impl Reply, Rejection> {
|
||||||
let rows = sqlx::query("SELECT id, filename, path, description FROM files ORDER BY created_at DESC LIMIT 500")
|
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)
|
.fetch_all(&pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
|
|
@ -158,7 +233,16 @@ async fn handle_list(pool: MySqlPool) -> Result<impl Reply, Rejection> {
|
||||||
let filename: String = r.get("filename");
|
let filename: String = r.get("filename");
|
||||||
let path: String = r.get("path");
|
let path: String = r.get("path");
|
||||||
let description: Option<String> = r.get("description");
|
let description: Option<String> = r.get("description");
|
||||||
serde_json::json!({"id": id, "filename": filename, "path": path, "description": description})
|
let pending: bool = r.get("pending_analysis");
|
||||||
|
let status: Option<String> = r.try_get("analysis_status").ok();
|
||||||
|
serde_json::json!({
|
||||||
|
"id": id,
|
||||||
|
"filename": filename,
|
||||||
|
"path": path,
|
||||||
|
"description": description,
|
||||||
|
"pending_analysis": pending,
|
||||||
|
"analysis_status": status
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,4 +6,4 @@ RUN npm ci
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm run format && npm run build
|
RUN npm run format && npm run build
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
CMD ["npm", "run", "preview"]
|
CMD ["node", "server.mjs"]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": "./"
|
"baseUrl": "./",
|
||||||
|
"lib": ["es2015", "dom"]
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
44
web-app/server.mjs
Normal file
44
web-app/server.mjs
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import express from 'express';
|
||||||
|
import path from 'node:path';
|
||||||
|
import helmet from 'helmet';
|
||||||
|
import cors from 'cors';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
const RUST_ENGINE_BASE = process.env.RUST_ENGINE_BASE || 'http://rust-engine:8000';
|
||||||
|
|
||||||
|
app.use(helmet());
|
||||||
|
app.use(cors());
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// Proxy minimal API needed by the UI to the rust-engine container
|
||||||
|
app.post('/api/files/import-demo', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const qs = req.url.includes('?') ? req.url.substring(req.url.indexOf('?')) : '';
|
||||||
|
const url = `${RUST_ENGINE_BASE}/api/files/import-demo${qs}`;
|
||||||
|
const upstream = await fetch(url, { method: 'POST' });
|
||||||
|
const text = await upstream.text();
|
||||||
|
res.status(upstream.status).type(upstream.headers.get('content-type') || 'application/json').send(text);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('import-demo proxy failed:', err);
|
||||||
|
res.status(502).json({ error: 'proxy_failed' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Serve static frontend
|
||||||
|
const distDir = path.resolve(__dirname, 'dist');
|
||||||
|
app.use(express.static(distDir));
|
||||||
|
|
||||||
|
// SPA fallback
|
||||||
|
app.get('*', (req, res) => {
|
||||||
|
res.sendFile(path.join(distDir, 'index.html'));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(PORT, '0.0.0.0', () => {
|
||||||
|
console.log(`Web app server listening on http://0.0.0.0:${PORT}`);
|
||||||
|
console.log(`Proxying to rust engine at ${RUST_ENGINE_BASE}`);
|
||||||
|
});
|
||||||
|
|
@ -1,14 +1,54 @@
|
||||||
import React from "react";
|
import React, { useMemo, useState } from "react";
|
||||||
|
import { motion } from "motion/react";
|
||||||
|
import { Rocket } from "lucide-react";
|
||||||
|
|
||||||
export default function ChatHeader({ title = "Title of Chat" }) {
|
export default function ChatHeader({ title = "Title of Chat" }) {
|
||||||
|
const isDebug = useMemo(() => {
|
||||||
|
const p = new URLSearchParams(window.location.search);
|
||||||
|
return p.get("debug") === "1";
|
||||||
|
}, []);
|
||||||
|
const [ingesting, setIngesting] = useState(false);
|
||||||
|
const [toast, setToast] = useState("");
|
||||||
|
|
||||||
|
async function triggerDemoIngest() {
|
||||||
|
try {
|
||||||
|
setIngesting(true);
|
||||||
|
const res = await fetch("/api/files/import-demo", { method: "POST" });
|
||||||
|
const json = await res.json().catch(() => ({}));
|
||||||
|
setToast(`Imported: ${json.imported ?? "?"}, Skipped: ${json.skipped ?? "?"}`);
|
||||||
|
setTimeout(() => setToast(""), 4000);
|
||||||
|
} catch (e) {
|
||||||
|
setToast("Import failed");
|
||||||
|
setTimeout(() => setToast(""), 4000);
|
||||||
|
} finally {
|
||||||
|
setIngesting(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex justify-center">
|
<div className="w-full flex justify-center">
|
||||||
<header className="text-slate-100 fixed top-4 ">
|
<header className="text-slate-100 fixed top-4 ">
|
||||||
<div>
|
<div className="flex items-center gap-3">
|
||||||
<h1 className="text-lg font-semibold shadow-md shadow-indigo-600 bg-gray-900 px-6 py-2 rounded-4xl border-2 border-gray-800">
|
<h1 className="text-lg font-semibold shadow-md shadow-indigo-600 bg-gray-900 px-6 py-2 rounded-4xl border-2 border-gray-800">
|
||||||
{title}
|
{title}
|
||||||
</h1>
|
</h1>
|
||||||
|
{isDebug && (
|
||||||
|
<motion.button
|
||||||
|
onClick={triggerDemoIngest}
|
||||||
|
className="bg-gray-800 border-2 border-gray-700 rounded-xl px-3 py-2 flex items-center gap-2"
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
disabled={ingesting}
|
||||||
|
>
|
||||||
|
<Rocket size={16} />
|
||||||
|
{ingesting ? "Seeding…" : "Seed Demo Data"}
|
||||||
|
</motion.button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{toast && (
|
||||||
|
<div className="mt-2 text-xs text-slate-300 bg-gray-800/80 border border-gray-700 rounded px-2 py-1 inline-block">
|
||||||
|
{toast}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</header>
|
</header>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue