Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,227 +1,265 @@
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
# -*- coding: utf-8 -*-
|
| 3 |
"""
|
| 4 |
-
MaterialMind
|
| 5 |
-
-
|
| 6 |
-
-
|
| 7 |
-
-
|
| 8 |
-
-
|
| 9 |
"""
|
| 10 |
-
import os, re, json, textwrap
|
| 11 |
-
from pathlib import Path
|
| 12 |
-
from typing import List, Tuple, Dict, Any
|
| 13 |
-
|
| 14 |
-
import gradio as gr
|
| 15 |
-
import requests
|
| 16 |
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
)
|
| 20 |
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
try:
|
| 46 |
-
return
|
| 47 |
except Exception:
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
|
|
|
|
|
|
| 53 |
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
{
|
| 56 |
"candidates": [
|
| 57 |
{
|
| 58 |
"name": "string",
|
| 59 |
-
"score": 0,
|
| 60 |
-
"
|
| 61 |
-
"reasons": ["
|
| 62 |
-
"tradeoffs": ["
|
| 63 |
-
"citations": ["[1]", "[
|
| 64 |
}
|
| 65 |
]
|
| 66 |
}
|
| 67 |
-
|
| 68 |
-
SCORING (absolute, not weighted):
|
| 69 |
-
- performance (0..100): strength/stiffness/thermal range vs user targets
|
| 70 |
-
- stability (0..100): corrosion/oxidation/chem/UV/thermal/creep, environment fit
|
| 71 |
-
- cost (0..100): relative cost vs user budget (If budget is "Not important", set cost=100)
|
| 72 |
-
- availability(0..100): manufacturability, supply forms/lead time
|
| 73 |
-
|
| 74 |
-
Total score = performance + stability + cost + availability (0..400). Be conservative; do not invent data.
|
| 75 |
-
|
| 76 |
-
2) After the JSON, add 3–6 concise bullets explaining trade-offs.
|
| 77 |
-
|
| 78 |
Rules:
|
| 79 |
-
- Use
|
| 80 |
-
-
|
| 81 |
-
-
|
|
|
|
| 82 |
"""
|
| 83 |
|
| 84 |
-
ANSWER_TEMPLATE = """User constraints
|
| 85 |
-
-
|
| 86 |
- Temperature: {temperature}
|
| 87 |
-
-
|
| 88 |
-
-
|
| 89 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
Explain trade-offs and include citations.
|
| 94 |
|
| 95 |
-
Context snippets (numbered)
|
| 96 |
{context}
|
| 97 |
|
| 98 |
-
Citations
|
| 99 |
{citations}
|
| 100 |
|
| 101 |
-
Now first output ONLY the JSON block
|
| 102 |
"""
|
| 103 |
|
| 104 |
-
def
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
try:
|
| 112 |
-
|
| 113 |
except Exception:
|
| 114 |
-
|
| 115 |
-
if last != -1:
|
| 116 |
-
try: return json.loads(s[:last+1])
|
| 117 |
-
except Exception: return None
|
| 118 |
-
return None
|
| 119 |
-
|
| 120 |
-
# -------------------- Build index once (your PDFs) --------------------
|
| 121 |
-
SOURCES_DIR = Path(os.getenv("SOURCES_DIR", "sources")).resolve()
|
| 122 |
-
INDEX = build_index_from_dir(SOURCES_DIR) # texts, metas, embs (L2-normalized)
|
| 123 |
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
if INDEX["embs"].shape[0] == 0:
|
| 132 |
-
return "No context available. Add PDFs to ./sources and redeploy.", None, None
|
| 133 |
|
| 134 |
-
|
| 135 |
-
q = (f"For {environment or 'general'} at {temperature or 'room temperature'}, shortlist materials that meet "
|
| 136 |
-
f"UTS ≥ {min_uts or '0'} MPa and density ≤ {max_density or '100'} g/cm^3; "
|
| 137 |
-
f"consider budget={budget or 'open'}, process={process or 'any'}.")
|
| 138 |
-
hits = retrieve(INDEX, q, k=int(topk))
|
| 139 |
if not hits:
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
environment=environment
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
budget=budget or "open",
|
| 150 |
-
process=process or "any",
|
| 151 |
-
pref_perf=pref_perf, pref_stab=pref_stab, pref_cost=pref_cost, pref_avail=pref_avail,
|
| 152 |
-
context=ctx, citations=cites
|
| 153 |
)
|
| 154 |
-
|
|
|
|
| 155 |
parsed = extract_json_block(raw) if raw else None
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
ss.get("performance","—"), ss.get("stability","—"),
|
| 169 |
-
ss.get("cost","—"), ss.get("availability","—"), reasons])
|
| 170 |
-
|
| 171 |
-
# Markdown table
|
| 172 |
-
table_md = "| " + " | ".join(headers) + " |\n|" + " --- |"*len(headers) + "\n"
|
| 173 |
-
for r in rows:
|
| 174 |
-
table_md += "| " + " | ".join(str(x) for x in r) + " |\n"
|
| 175 |
-
|
| 176 |
-
# Cards
|
| 177 |
-
cards = []
|
| 178 |
-
for i, c in enumerate(sorted(cands, key=lambda x: x.get("score",0), reverse=True), 1):
|
| 179 |
-
ss = c.get("subscores", {})
|
| 180 |
-
card = f"**{i}. {c.get('name','?')}** \n"
|
| 181 |
-
card += f"Score {c.get('score',0)} (perf {ss.get('performance','—')}, stab {ss.get('stability','—')}, cost {ss.get('cost','—')}, avail {ss.get('availability','—')})\n\n"
|
| 182 |
-
if c.get("tradeoffs"):
|
| 183 |
-
card += "**Trade-offs:**\n- " + "\n- ".join(c["tradeoffs"]) + "\n\n"
|
| 184 |
-
if c.get("citations"):
|
| 185 |
-
card += "**Citations:** " + ", ".join(c["citations"])
|
| 186 |
-
cards.append(card)
|
| 187 |
-
cards_md = "\n---\n".join(cards)
|
| 188 |
-
|
| 189 |
-
return table_md + "\n\n" + raw, cards_md, cites
|
| 190 |
-
|
| 191 |
-
# -------------------- Gradio UI --------------------
|
| 192 |
-
with gr.Blocks(title="MaterialMind") as demo:
|
| 193 |
-
gr.Markdown("## MaterialMind — ranked materials shortlist with page-level citations")
|
| 194 |
-
with gr.Row():
|
| 195 |
-
environment = gr.Textbox(label="Application", placeholder="seawater / sour service / high-T oxidation")
|
| 196 |
-
temperature = gr.Textbox(label="Temperature", placeholder="e.g., 20–25 °C")
|
| 197 |
-
with gr.Row():
|
| 198 |
-
min_uts = gr.Textbox(label="Min UTS (MPa)", value="0")
|
| 199 |
-
max_density = gr.Textbox(label="Max density (g/cm³)", value="100")
|
| 200 |
-
with gr.Row():
|
| 201 |
-
budget = gr.Dropdown(["open","low","medium","high","Not important"], value="open", label="Budget")
|
| 202 |
-
process = gr.Textbox(label="Process", placeholder="wrought / casting / AM / any", value="any")
|
| 203 |
-
|
| 204 |
-
gr.Markdown("**Priorities (qualitative; scoring is absolute 0..100 each, total 0..400)**")
|
| 205 |
-
with gr.Row():
|
| 206 |
-
pref_perf = gr.Dropdown(["Very high","High","Medium","Low","Very low"], value="High", label="Performance")
|
| 207 |
-
pref_stab = gr.Dropdown(["Very high","High","Medium","Low","Very low"], value="High", label="Stability")
|
| 208 |
-
pref_cost = gr.Dropdown(["Not important","High","Medium","Low","Very low"], value="Medium", label="Cost")
|
| 209 |
-
pref_avail = gr.Dropdown(["Very high","High","Medium","Low","Very low"], value="Medium", label="Availability")
|
| 210 |
-
|
| 211 |
-
topk = gr.Slider(3, 10, step=1, value=5, label="Top-k context pages")
|
| 212 |
-
|
| 213 |
-
run_btn = gr.Button("Get ranked shortlist", variant="primary")
|
| 214 |
-
out_table = gr.Markdown(label="Shortlist & raw model output")
|
| 215 |
-
out_cards = gr.Markdown(label="Material cards")
|
| 216 |
-
out_cites = gr.Markdown(label="Citations (source mapping)")
|
| 217 |
-
|
| 218 |
-
run_btn.click(
|
| 219 |
-
recommend,
|
| 220 |
-
inputs=[environment, temperature, min_uts, max_density, budget, process,
|
| 221 |
-
pref_perf, pref_stab, pref_cost, pref_avail, topk],
|
| 222 |
-
outputs=[out_table, out_cards, out_cites],
|
| 223 |
-
api_name="recommend"
|
| 224 |
)
|
| 225 |
|
| 226 |
if __name__ == "__main__":
|
| 227 |
-
|
|
|
|
|
|
|
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
# -*- coding: utf-8 -*-
|
| 3 |
"""
|
| 4 |
+
MaterialMind – Hugging Face Spaces app
|
| 5 |
+
- Same UI & templates you already have
|
| 6 |
+
- Reads PDFs from HF dataset Azizahalq/materialmind-corpus
|
| 7 |
+
- Builds/updates a local Chroma index at startup
|
| 8 |
+
- Calls an API LLM (OpenAI or Together) via Space secrets
|
| 9 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
+
import os, re, json, textwrap
|
| 12 |
+
import subprocess, shutil
|
| 13 |
+
from typing import List, Tuple, Any, Dict
|
| 14 |
+
from flask import Flask, request, render_template, redirect, url_for, flash
|
| 15 |
+
from flask_cors import CORS
|
| 16 |
+
from filelock import FileLock
|
| 17 |
+
|
| 18 |
+
# Silence tokenizers warning in HF
|
| 19 |
+
os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
|
| 20 |
+
|
| 21 |
+
# ---- RAG helpers (portable) ----
|
| 22 |
+
from rag_mini import (
|
| 23 |
+
ensure_dirs, bootstrap_corpus_and_index, search,
|
| 24 |
+
DATA_DIR, DEFAULT_TOPK
|
| 25 |
)
|
| 26 |
|
| 27 |
+
app = Flask(__name__)
|
| 28 |
+
app.secret_key = os.getenv("FLASK_SECRET", "change-me")
|
| 29 |
+
CORS(app)
|
| 30 |
+
|
| 31 |
+
# HF runs on port 7860
|
| 32 |
+
PORT = int(os.environ.get("PORT", "7860"))
|
| 33 |
+
|
| 34 |
+
LOCK_PATH = (DATA_DIR.parent / ".rag_lock")
|
| 35 |
+
DEFAULT_MODEL = os.getenv("LLM_MODEL", "gpt-4o-mini")
|
| 36 |
+
LLM_PROVIDER = os.getenv("LLM_PROVIDER", "openai") # "openai" or "together"
|
| 37 |
+
LLM_API_KEY = os.getenv("LLM_API_KEY", "")
|
| 38 |
+
|
| 39 |
+
# ---------- LLM caller (remote) ----------
|
| 40 |
+
def call_llm(model: str, system_prompt: str, user_prompt: str) -> str:
|
| 41 |
+
provider = LLM_PROVIDER.lower().strip()
|
| 42 |
+
if provider == "openai":
|
| 43 |
+
try:
|
| 44 |
+
from openai import OpenAI
|
| 45 |
+
client = OpenAI(api_key=LLM_API_KEY)
|
| 46 |
+
resp = client.chat.completions.create(
|
| 47 |
+
model=model,
|
| 48 |
+
temperature=0.2,
|
| 49 |
+
messages=[
|
| 50 |
+
{"role": "system", "content": system_prompt},
|
| 51 |
+
{"role": "user", "content": user_prompt},
|
| 52 |
+
],
|
| 53 |
+
)
|
| 54 |
+
return resp.choices[0].message.content or ""
|
| 55 |
+
except Exception as e:
|
| 56 |
+
return f"[Error] OpenAI call failed: {e}"
|
| 57 |
+
|
| 58 |
+
elif provider == "together":
|
| 59 |
+
# Simple Together REST call (instruct/chat style)
|
| 60 |
+
import requests
|
| 61 |
+
url = "https://api.together.xyz/v1/chat/completions"
|
| 62 |
+
headers = {"Authorization": f"Bearer {LLM_API_KEY}", "Content-Type": "application/json"}
|
| 63 |
+
payload = {
|
| 64 |
+
"model": model,
|
| 65 |
+
"temperature": 0.2,
|
| 66 |
+
"messages": [
|
| 67 |
+
{"role": "system", "content": system_prompt},
|
| 68 |
+
{"role": "user", "content": user_prompt},
|
| 69 |
+
],
|
| 70 |
+
}
|
| 71 |
+
try:
|
| 72 |
+
r = requests.post(url, headers=headers, json=payload, timeout=120)
|
| 73 |
+
r.raise_for_status()
|
| 74 |
+
j = r.json()
|
| 75 |
+
return j["choices"][0]["message"]["content"]
|
| 76 |
+
except Exception as e:
|
| 77 |
+
return f"[Error] Together call failed: {e}"
|
| 78 |
+
|
| 79 |
+
return "[Error] Unknown LLM_PROVIDER. Set LLM_PROVIDER to 'openai' or 'together'."
|
| 80 |
+
|
| 81 |
+
def extract_json_block(text: str):
|
| 82 |
+
m = re.search(r"```json\s*(\{.*?\})\s*```", text, flags=re.S | re.I)
|
| 83 |
+
s = m.group(1) if m else None
|
| 84 |
+
if not s:
|
| 85 |
+
m2 = re.search(r"(\{(?:[^{}]|(?1))*\})", text, flags=re.S)
|
| 86 |
+
s = m2.group(1) if m2 else None
|
| 87 |
+
if not s:
|
| 88 |
+
return None
|
| 89 |
try:
|
| 90 |
+
return json.loads(s)
|
| 91 |
except Exception:
|
| 92 |
+
last = s.rfind("}")
|
| 93 |
+
if last != -1:
|
| 94 |
+
try:
|
| 95 |
+
return json.loads(s[:last+1])
|
| 96 |
+
except Exception:
|
| 97 |
+
return None
|
| 98 |
+
return None
|
| 99 |
|
| 100 |
+
def normalize_candidates_for_display(cands: List[Dict[str, Any]], max_total: float = 400.0) -> List[Dict[str, Any]]:
|
| 101 |
+
def _to_float(x):
|
| 102 |
+
try: return float(x)
|
| 103 |
+
except: return None
|
| 104 |
+
|
| 105 |
+
for c in cands:
|
| 106 |
+
if "score_pct" in c and c["score_pct"] is not None:
|
| 107 |
+
try:
|
| 108 |
+
p = float(c["score_pct"])
|
| 109 |
+
c["score_pct"] = max(0.0, min(100.0, p))
|
| 110 |
+
c.setdefault("score_raw", c["score_pct"] * 4.0)
|
| 111 |
+
continue
|
| 112 |
+
except: pass
|
| 113 |
+
|
| 114 |
+
raw = None
|
| 115 |
+
v = c.get("score")
|
| 116 |
+
if isinstance(v, (int, float)):
|
| 117 |
+
f = float(v)
|
| 118 |
+
raw = (f * max_total) if f <= 1.5 else f
|
| 119 |
+
elif isinstance(v, str):
|
| 120 |
+
s = v.strip()
|
| 121 |
+
m = re.search(r"^\s*([\d.]+)\s*/\s*([\d.]+)\s*$", s)
|
| 122 |
+
if m:
|
| 123 |
+
num, den = _to_float(m.group(1)), _to_float(m.group(2))
|
| 124 |
+
if num is not None and den and den > 0: raw = max_total * (num/den)
|
| 125 |
+
if raw is None:
|
| 126 |
+
m2 = re.search(r"^\s*([\d.]+)\s*%\s*$", s)
|
| 127 |
+
if m2:
|
| 128 |
+
p = _to_float(m2.group(1))
|
| 129 |
+
if p is not None: raw = max_total * (p/100.0)
|
| 130 |
+
if raw is None:
|
| 131 |
+
f = _to_float(s)
|
| 132 |
+
if f is not None: raw = (f * max_total) if f <= 1.5 else f
|
| 133 |
+
|
| 134 |
+
if raw is None:
|
| 135 |
+
subs = c.get("subscores") or {}
|
| 136 |
+
if isinstance(subs, dict) and subs:
|
| 137 |
+
raw = sum(max(0.0, min(100.0, _to_float(v) or 0.0)) for v in subs.values())
|
| 138 |
+
|
| 139 |
+
raw = 0.0 if raw is None else max(0.0, min(max_total, float(raw)))
|
| 140 |
+
c["score_raw"] = raw
|
| 141 |
+
c["score_pct"] = round((raw / max_total) * 100.0, 1)
|
| 142 |
+
|
| 143 |
+
cands.sort(key=lambda z: z.get("score_raw", 0.0), reverse=True)
|
| 144 |
+
return cands
|
| 145 |
+
|
| 146 |
+
# ---------- Prompt text ----------
|
| 147 |
+
SYSTEM_RULES = """You are MaterialMind, a materials-selection assistant.
|
| 148 |
+
Return two things:
|
| 149 |
+
1) JSON with a ranked shortlist:
|
| 150 |
{
|
| 151 |
"candidates": [
|
| 152 |
{
|
| 153 |
"name": "string",
|
| 154 |
+
"score": 0, // 0..400 (sum of 4 independent 0..100 utilities)
|
| 155 |
+
"score_pct": 0, // score/4 -> 0..100 for display
|
| 156 |
+
"reasons": ["..."],
|
| 157 |
+
"tradeoffs": ["..."],
|
| 158 |
+
"citations": ["[1]", "[2]"]
|
| 159 |
}
|
| 160 |
]
|
| 161 |
}
|
| 162 |
+
2) After the JSON, 3–6 concise bullets on trade-offs.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
Rules:
|
| 164 |
+
- Use only provided context; cite with [1], [2], etc. No fabrication.
|
| 165 |
+
- Utilities per criterion are in [0,1]. Lower cost => higher cost-utility.
|
| 166 |
+
- Weights (performance, stability, cost, availability) are independent 0..100 (NOT normalized).
|
| 167 |
+
- Prefer pitting/crevice metrics in chloride questions; keep units explicit.
|
| 168 |
"""
|
| 169 |
|
| 170 |
+
ANSWER_TEMPLATE = """User constraints:
|
| 171 |
+
- Environment: {environment}
|
| 172 |
- Temperature: {temperature}
|
| 173 |
+
- Min UTS (MPa): {min_uts}
|
| 174 |
+
- Max density (g/cm^3): {max_density}
|
| 175 |
+
- Budget: {budget}
|
| 176 |
+
- Process: {process}
|
| 177 |
+
|
| 178 |
+
Independent priorities (0..100 each):
|
| 179 |
+
- performance={w_perf}, stability={w_stab}, cost={w_cost}, availability={w_avail}
|
| 180 |
|
| 181 |
+
Question:
|
| 182 |
+
{question}
|
|
|
|
| 183 |
|
| 184 |
+
Context snippets (numbered):
|
| 185 |
{context}
|
| 186 |
|
| 187 |
+
Citations:
|
| 188 |
{citations}
|
| 189 |
|
| 190 |
+
Now, first output ONLY the JSON block, then the short narrative.
|
| 191 |
"""
|
| 192 |
|
| 193 |
+
def format_context(hits: List[Tuple[str, str]]) -> Tuple[str, str]:
|
| 194 |
+
blocks, cites = [], []
|
| 195 |
+
for i, (text, cite) in enumerate(hits, 1):
|
| 196 |
+
snippet = textwrap.shorten(text.replace("\n", " "), width=450, placeholder=" …")
|
| 197 |
+
blocks.append(f"[{i}] {snippet}")
|
| 198 |
+
cites.append(f"[{i}] {cite}")
|
| 199 |
+
return "\n".join(blocks), "\n".join(cites)
|
| 200 |
+
|
| 201 |
+
# ---------- Routes ----------
|
| 202 |
+
@app.get("/")
|
| 203 |
+
def index():
|
| 204 |
+
return render_template("index.html", default_model=DEFAULT_MODEL, default_k=DEFAULT_TOPK)
|
| 205 |
+
|
| 206 |
+
@app.post("/recommend")
|
| 207 |
+
def recommend():
|
| 208 |
+
environment = request.form.get("environment", "").strip() or "seawater"
|
| 209 |
+
temperature = request.form.get("temperature", "").strip() or "20–25 °C"
|
| 210 |
+
min_uts = request.form.get("min_uts", "").strip() or "0"
|
| 211 |
+
max_density = request.form.get("max_density", "").strip() or "100"
|
| 212 |
+
budget = request.form.get("budget", "").strip() or "open"
|
| 213 |
+
process = request.form.get("process", "").strip() or "any"
|
| 214 |
+
|
| 215 |
+
# Hidden numeric weights set by JS (0..100 each; independent)
|
| 216 |
+
w_perf = request.form.get("w_perf", "75")
|
| 217 |
+
w_stab = request.form.get("w_stab", "100")
|
| 218 |
+
w_cost = request.form.get("w_cost", "75")
|
| 219 |
+
w_avail = request.form.get("w_avail", "75")
|
| 220 |
+
|
| 221 |
try:
|
| 222 |
+
k = int(request.form.get("k", DEFAULT_TOPK))
|
| 223 |
except Exception:
|
| 224 |
+
k = DEFAULT_TOPK
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 225 |
|
| 226 |
+
question = (
|
| 227 |
+
f"For {environment} at {temperature}, shortlist materials that meet "
|
| 228 |
+
f"UTS ≥ {min_uts} MPa and density ≤ {max_density} g/cm^3. "
|
| 229 |
+
f"Consider budget={budget} and process={process}. "
|
| 230 |
+
f"Rank by performance, stability, cost, and availability."
|
| 231 |
+
)
|
|
|
|
|
|
|
|
|
|
| 232 |
|
| 233 |
+
hits = search(question, k=k)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 234 |
if not hits:
|
| 235 |
+
flash("No context found. Make sure your dataset is reachable and indexed.", "error")
|
| 236 |
+
return redirect(url_for("index"))
|
| 237 |
+
|
| 238 |
+
ctx, cites = format_context(hits)
|
| 239 |
+
user_prompt = ANSWER_TEMPLATE.format(
|
| 240 |
+
environment=environment, temperature=temperature, min_uts=min_uts,
|
| 241 |
+
max_density=max_density, budget=budget, process=process,
|
| 242 |
+
w_perf=w_perf, w_stab=w_stab, w_cost=w_cost, w_avail=w_avail,
|
| 243 |
+
question=question, context=ctx, citations=cites
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
)
|
| 245 |
+
|
| 246 |
+
raw = call_llm(DEFAULT_MODEL, SYSTEM_RULES, user_prompt)
|
| 247 |
parsed = extract_json_block(raw) if raw else None
|
| 248 |
+
candidates = (parsed or {}).get("candidates", []) if parsed else []
|
| 249 |
+
candidates = normalize_candidates_for_display(candidates, max_total=400.0)
|
| 250 |
+
|
| 251 |
+
return render_template(
|
| 252 |
+
"results.html",
|
| 253 |
+
candidates=candidates,
|
| 254 |
+
citations=cites.splitlines(),
|
| 255 |
+
environment=environment,
|
| 256 |
+
temperature=temperature,
|
| 257 |
+
raw_output=raw,
|
| 258 |
+
default_model=DEFAULT_MODEL,
|
| 259 |
+
default_k=k,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
)
|
| 261 |
|
| 262 |
if __name__ == "__main__":
|
| 263 |
+
ensure_dirs()
|
| 264 |
+
bootstrap_corpus_and_index() # <-- download dataset + build/update index
|
| 265 |
+
app.run(host="0.0.0.0", port=PORT, debug=False)
|