Kogann commited on
Commit
d0d7f27
·
verified ·
1 Parent(s): 7329a2f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +52 -46
app.py CHANGED
@@ -8,12 +8,11 @@ Data sources, per the assignment constraints:
8
  * Embedding model -> read from the HF MODEL repo (BAAI/bge-small-en-v1.5)
9
  * Embeddings -> stored in THIS Space repo, with a dataset-repo fallback
10
 
11
- Runs on free-tier CPU. The three Quick Starters are served from a pre-generated
12
- cache so the common path is instant; the language model is loaded lazily and
13
- only when a custom questionnaire is submitted.
14
  """
15
 
16
- import os, json, re
17
  import numpy as np
18
  import pandas as pd
19
  import gradio as gr
@@ -58,22 +57,28 @@ index.add(embeddings.astype("float32"))
58
  encoder = SentenceTransformer(EMB_MODEL_ID, device="cpu")
59
  print(f"Ready: {index.ntotal:,} stocks | encoder {EMB_MODEL_ID}")
60
 
61
- # The generation model is heavy on CPU, so it is loaded on FIRST USE only.
62
- # Quick Starters never trigger it — they are served from the cache.
 
 
 
 
 
 
63
  GEN_MODEL_ID = "HuggingFaceTB/SmolLM2-1.7B-Instruct"
64
- _gen = {}
65
-
66
- def _load_generator():
67
- if "model" not in _gen:
68
- print(f"Lazy-loading {GEN_MODEL_ID}...")
69
- tok = AutoTokenizer.from_pretrained(GEN_MODEL_ID)
70
- tok.padding_side = "left" # required for batched generation
71
- if tok.pad_token is None:
72
- tok.pad_token = tok.eos_token
73
- m = AutoModelForCausalLM.from_pretrained(GEN_MODEL_ID, dtype=torch.float32)
74
- m.eval()
75
- _gen.update(tok=tok, model=m)
76
- return _gen["tok"], _gen["model"]
77
 
78
  # ---------------------------------------------------------------------------
79
  # QUESTIONNAIRE -> QUERY
@@ -147,8 +152,6 @@ def build_query(risks, sectors, markets, style, caps):
147
  if risk_phrase:
148
  parts.append(risk_phrase)
149
  q = ". ".join([p for p in parts if p])
150
- # If nothing at all was selected, fall back to the project's core intent so
151
- # the search still has something meaningful to match against.
152
  return q or "a stock that helps protect purchasing power against inflation"
153
 
154
  def embed_query(text):
@@ -264,19 +267,17 @@ def _stock_prompt(risks, style, r):
264
  f"investor and note one caveat.")
265
 
266
  def _generate_sentences(prompts):
267
- """Runs on CPU. Errors are returned as data rather than raised, so a
268
- generation failure never takes down the recommendation table."""
269
- import traceback
270
  try:
271
- tok, model = _load_generator()
272
- enc = tok(prompts, return_tensors="pt", padding=True)
273
  with torch.no_grad():
274
- out = model.generate(**enc, max_new_tokens=110, do_sample=True,
275
- temperature=0.4, top_p=0.9,
276
- repetition_penalty=1.1,
277
- pad_token_id=tok.pad_token_id)
278
- sents = tok.batch_decode(out[:, enc["input_ids"].shape[1]:],
279
- skip_special_tokens=True)
280
  return {"ok": True, "sentences": sents}
281
  except Exception:
282
  return {"ok": False, "traceback": traceback.format_exc()}
@@ -285,18 +286,24 @@ def generate_rationale(risks, amount, sectors, markets, style, recs):
285
  """One sentence per stock, generated in a single batched pass. All factual
286
  content — tickers and figures — is printed from the DataFrame, so it cannot
287
  be hallucinated."""
288
- tok, _ = _load_generator()
289
- prompts = [tok.apply_chat_template(
290
- [{"role": "system", "content": STOCK_SYSTEM_PROMPT},
291
- {"role": "user", "content": _stock_prompt(risks, style, r)}],
292
- tokenize=False, add_generation_prompt=True) for _, r in recs.iterrows()]
293
-
294
- result = _generate_sentences(prompts)
295
- if not result.get("ok"):
 
 
 
 
 
 
 
296
  return ("_Recommendations above are complete. The written explanation "
297
  "could not be generated._\n\n```\n"
298
- + result.get("traceback", "no traceback")[-1500:] + "\n```")
299
- sents = result["sentences"]
300
 
301
  amt = parse_amount(amount)
302
  rl = "/".join(_as_list(risks)).lower() or "flexible"
@@ -353,19 +360,18 @@ def _prettify(frame):
353
  # ---------------------------------------------------------------------------
354
  def run_custom(risks, amount, sectors, markets, style, caps):
355
  """Yields twice: the table appears immediately, then the explanation, so a
356
- CPU generation of roughly a minute does not look like a frozen page."""
357
  recs, note, _query = recommend(risks, amount, sectors, markets, style, caps)
358
  if recs.empty:
359
  yield pd.DataFrame(), note
360
  return
361
  pretty = _prettify(recs)
362
- yield pretty, (note + "\n⏳ Writing your personalised explanation… "
363
- "(about a minute on free CPU)").strip()
364
  text = generate_rationale(risks, amount, sectors, markets, style, recs)
365
  yield pretty, (note + "\n" + text).strip()
366
 
367
  def run_quickstart(name):
368
- """Served from the pre-generated cache — instant, no model load."""
369
  entry = QUICKSTART_CACHE[name]
370
  tbl = pd.DataFrame(entry["table"])
371
  for c in ["beta", DIV_COL, "cagr_10yr_pct", "price_usd", "similarity",
 
8
  * Embedding model -> read from the HF MODEL repo (BAAI/bge-small-en-v1.5)
9
  * Embeddings -> stored in THIS Space repo, with a dataset-repo fallback
10
 
11
+ The three Quick Starters are served from a pre-generated cache and never touch
12
+ the language model, so the common path is instant.
 
13
  """
14
 
15
+ import os, json, re, traceback
16
  import numpy as np
17
  import pandas as pd
18
  import gradio as gr
 
57
  encoder = SentenceTransformer(EMB_MODEL_ID, device="cpu")
58
  print(f"Ready: {index.ntotal:,} stocks | encoder {EMB_MODEL_ID}")
59
 
60
+ # ---------------------------------------------------------------------------
61
+ # GENERATION MODEL
62
+ # Loaded at STARTUP rather than on first request. Lazy loading pushed a ~60s
63
+ # download-and-load into the first user's request, which exceeded the request
64
+ # timeout and surfaced as a bare "Error". Paying the cost once at boot keeps
65
+ # every request fast. If it fails, the app still serves recommendations —
66
+ # retrieval does not depend on the language model.
67
+ # ---------------------------------------------------------------------------
68
  GEN_MODEL_ID = "HuggingFaceTB/SmolLM2-1.7B-Instruct"
69
+ gen_tok, gen_model, GEN_LOAD_ERROR = None, None, None
70
+ try:
71
+ print(f"Loading {GEN_MODEL_ID} ...")
72
+ gen_tok = AutoTokenizer.from_pretrained(GEN_MODEL_ID)
73
+ gen_tok.padding_side = "left" # required for batched generation
74
+ if gen_tok.pad_token is None:
75
+ gen_tok.pad_token = gen_tok.eos_token
76
+ gen_model = AutoModelForCausalLM.from_pretrained(GEN_MODEL_ID, dtype=torch.float32)
77
+ gen_model.eval()
78
+ print("Generation model ready.")
79
+ except Exception as e:
80
+ GEN_LOAD_ERROR = f"{type(e).__name__}: {e}"
81
+ print("Generation model FAILED to load:", GEN_LOAD_ERROR)
82
 
83
  # ---------------------------------------------------------------------------
84
  # QUESTIONNAIRE -> QUERY
 
152
  if risk_phrase:
153
  parts.append(risk_phrase)
154
  q = ". ".join([p for p in parts if p])
 
 
155
  return q or "a stock that helps protect purchasing power against inflation"
156
 
157
  def embed_query(text):
 
267
  f"investor and note one caveat.")
268
 
269
  def _generate_sentences(prompts):
270
+ """Errors are returned as data rather than raised, so a generation failure
271
+ never takes down the recommendation table."""
 
272
  try:
273
+ enc = gen_tok(prompts, return_tensors="pt", padding=True)
 
274
  with torch.no_grad():
275
+ out = gen_model.generate(**enc, max_new_tokens=110, do_sample=True,
276
+ temperature=0.4, top_p=0.9,
277
+ repetition_penalty=1.1,
278
+ pad_token_id=gen_tok.pad_token_id)
279
+ sents = gen_tok.batch_decode(out[:, enc["input_ids"].shape[1]:],
280
+ skip_special_tokens=True)
281
  return {"ok": True, "sentences": sents}
282
  except Exception:
283
  return {"ok": False, "traceback": traceback.format_exc()}
 
286
  """One sentence per stock, generated in a single batched pass. All factual
287
  content — tickers and figures — is printed from the DataFrame, so it cannot
288
  be hallucinated."""
289
+ if gen_model is None:
290
+ return ("_Recommendations above are complete. The language model is "
291
+ f"unavailable._\n\n`{GEN_LOAD_ERROR}`")
292
+ try:
293
+ prompts = [gen_tok.apply_chat_template(
294
+ [{"role": "system", "content": STOCK_SYSTEM_PROMPT},
295
+ {"role": "user", "content": _stock_prompt(risks, style, r)}],
296
+ tokenize=False, add_generation_prompt=True) for _, r in recs.iterrows()]
297
+ result = _generate_sentences(prompts)
298
+ if not result.get("ok"):
299
+ return ("_Recommendations above are complete. The written explanation "
300
+ "could not be generated._\n\n```\n"
301
+ + result.get("traceback", "")[-1200:] + "\n```")
302
+ sents = result["sentences"]
303
+ except Exception:
304
  return ("_Recommendations above are complete. The written explanation "
305
  "could not be generated._\n\n```\n"
306
+ + traceback.format_exc()[-1200:] + "\n```")
 
307
 
308
  amt = parse_amount(amount)
309
  rl = "/".join(_as_list(risks)).lower() or "flexible"
 
360
  # ---------------------------------------------------------------------------
361
  def run_custom(risks, amount, sectors, markets, style, caps):
362
  """Yields twice: the table appears immediately, then the explanation, so a
363
+ generation of some seconds does not look like a frozen page."""
364
  recs, note, _query = recommend(risks, amount, sectors, markets, style, caps)
365
  if recs.empty:
366
  yield pd.DataFrame(), note
367
  return
368
  pretty = _prettify(recs)
369
+ yield pretty, (note + "\n⏳ Writing your personalised explanation…").strip()
 
370
  text = generate_rationale(risks, amount, sectors, markets, style, recs)
371
  yield pretty, (note + "\n" + text).strip()
372
 
373
  def run_quickstart(name):
374
+ """Served from the pre-generated cache — instant, no model call."""
375
  entry = QUICKSTART_CACHE[name]
376
  tbl = pd.DataFrame(entry["table"])
377
  for c in ["beta", DIV_COL, "cagr_10yr_pct", "price_usd", "similarity",