Prepared demo files and demo explanation file. Added debug only button to trigger demo file ingest on the server to queue and prepare the files. Added small expressjs server for talking between the web app and the rust engine containers.
This commit is contained in:
parent
381b7b8858
commit
a03969e497
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
|
||||
2. set env DATABASE_URL and QDRANT_URL
|
||||
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
|
||||
|
||||
|
|
|
|||
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 {
|
||||
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
|
||||
let upload = warp::path("files")
|
||||
.and(warp::post())
|
||||
|
|
@ -64,7 +75,7 @@ pub fn routes(pool: MySqlPool) -> impl Filter<Extract = impl Reply, Error = Reje
|
|||
.and(pool_filter.clone())
|
||||
.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)
|
||||
}
|
||||
|
||||
|
|
@ -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})))
|
||||
}
|
||||
|
||||
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> {
|
||||
if let Some(row) = sqlx::query("SELECT path FROM files WHERE 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> {
|
||||
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)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
|
|
@ -158,7 +233,16 @@ async fn handle_list(pool: MySqlPool) -> Result<impl Reply, Rejection> {
|
|||
let filename: String = r.get("filename");
|
||||
let path: String = r.get("path");
|
||||
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();
|
||||
|
||||
|
|
|
|||
|
|
@ -6,4 +6,4 @@ RUN npm ci
|
|||
COPY . .
|
||||
RUN npm run format && npm run build
|
||||
EXPOSE 3000
|
||||
CMD ["npm", "run", "preview"]
|
||||
CMD ["node", "server.mjs"]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./"
|
||||
"baseUrl": "./",
|
||||
"lib": ["es2015", "dom"]
|
||||
},
|
||||
"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" }) {
|
||||
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 (
|
||||
<div className="w-full flex justify-center">
|
||||
<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">
|
||||
{title}
|
||||
</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>
|
||||
{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>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue