Che237 commited on
Commit
5e50189
·
verified ·
1 Parent(s): 02419cd

Replace Gemini with in-Space open-source LLM (Qwen3.5-2B via llama.cpp)

Browse files
Files changed (2) hide show
  1. app.py +23 -9
  2. requirements.txt +3 -1
app.py CHANGED
@@ -68,6 +68,7 @@ LOCAL_LLM_FILE = os.environ.get("LOCAL_LLM_FILE", "Qwen3.5-2B-Q4_K_M.gguf")
68
  LOCAL_LLM_CTX = int(os.environ.get("LOCAL_LLM_CTX", "4096"))
69
  LOCAL_LLM_THREADS = int(os.environ.get("LOCAL_LLM_THREADS", "2"))
70
  LOCAL_LLM_MAX_TOKENS = int(os.environ.get("LOCAL_LLM_MAX_TOKENS", "320"))
 
71
  LOCAL_LLM_MAX_INPUT_CHARS = int(os.environ.get("LOCAL_LLM_MAX_INPUT_CHARS", "2400"))
72
  LOCAL_LLM_TIME_BUDGET_S = float(os.environ.get("LOCAL_LLM_TIME_BUDGET_S", "22"))
73
  LOCAL_LLM_QUEUE_TIMEOUT_S = float(os.environ.get("LOCAL_LLM_QUEUE_TIMEOUT_S", "5"))
@@ -306,8 +307,8 @@ class MLModelLoader:
306
  loaded += 1
307
  logger.info(f"✅ Loaded model from Hub: {name}")
308
  continue
309
- except Exception:
310
- pass
311
 
312
  # 2. Try all local search directories
313
  for sdir in search_dirs:
@@ -593,30 +594,33 @@ class LocalLLM:
593
  f"<|im_start|>assistant\n{self._prefill}"
594
  )
595
  stream = self.llm.create_completion(
596
- prompt, max_tokens=max_tokens, temperature=0.7, top_p=0.8, top_k=20,
597
  presence_penalty=1.5, stop=["<|im_end|>", "<|endoftext|>"], stream=True,
598
  )
599
  read = lambda c: c["choices"][0].get("text") or ""
600
  else:
601
  stream = self.llm.create_chat_completion(
602
  messages=[{"role": "system", "content": system}, {"role": "user", "content": user}],
603
- max_tokens=max_tokens, temperature=0.7, top_p=0.8, stream=True,
604
  )
605
  read = lambda c: c["choices"][0].get("delta", {}).get("content") or ""
606
  pieces: List[str] = []
607
  truncated = False
608
  for chunk in stream:
609
  pieces.append(read(chunk))
 
 
610
  if time.monotonic() - started > LOCAL_LLM_TIME_BUDGET_S:
611
  truncated = True
612
  break
613
  import re
614
  text = re.sub(r"<think>.*?</think>", "", "".join(pieces), flags=re.S).strip()
615
  if truncated:
616
- # Out of time: end on the last complete sentence or line
617
- cut = max(text.rfind(". "), text.rfind(".\n"), text.rfind("\n"))
618
- if cut > len(text) * 0.5:
619
- text = text[:cut + 1].strip()
 
620
  if not text:
621
  return {**base, "response": None, "error": "local LLM returned no text"}
622
  return {
@@ -811,6 +815,16 @@ class TransformerModelLoader:
811
  logger.warning(f"Hosted LLM error: {type(e).__name__}: {str(e)[:160]} — using the local model")
812
  return None
813
 
 
 
 
 
 
 
 
 
 
 
814
  def security_chat(self, query: str, max_tokens: int = 512, allow_local: bool = True) -> Dict:
815
  """Cybersecurity Q&A.
816
  1. Hosted model through HF Inference Providers, while the account has credit.
@@ -822,7 +836,7 @@ class TransformerModelLoader:
822
  return hosted
823
  if not allow_local:
824
  return {"model": "security-llm", "response": None, "error": "hosted LLM unavailable"}
825
- result = local_llm.chat(self.SYSTEM_PROMPT, query, max_tokens=max_tokens)
826
  if result.get("response"):
827
  self.last_engine = "local"
828
  return result
 
68
  LOCAL_LLM_CTX = int(os.environ.get("LOCAL_LLM_CTX", "4096"))
69
  LOCAL_LLM_THREADS = int(os.environ.get("LOCAL_LLM_THREADS", "2"))
70
  LOCAL_LLM_MAX_TOKENS = int(os.environ.get("LOCAL_LLM_MAX_TOKENS", "320"))
71
+ LOCAL_LLM_TEMPERATURE = float(os.environ.get("LOCAL_LLM_TEMPERATURE", "0.3"))
72
  LOCAL_LLM_MAX_INPUT_CHARS = int(os.environ.get("LOCAL_LLM_MAX_INPUT_CHARS", "2400"))
73
  LOCAL_LLM_TIME_BUDGET_S = float(os.environ.get("LOCAL_LLM_TIME_BUDGET_S", "22"))
74
  LOCAL_LLM_QUEUE_TIMEOUT_S = float(os.environ.get("LOCAL_LLM_QUEUE_TIMEOUT_S", "5"))
 
307
  loaded += 1
308
  logger.info(f"✅ Loaded model from Hub: {name}")
309
  continue
310
+ except Exception as e:
311
+ logger.warning(f"Could not load {name} from the Hub: {type(e).__name__}: {str(e)[:200]}")
312
 
313
  # 2. Try all local search directories
314
  for sdir in search_dirs:
 
594
  f"<|im_start|>assistant\n{self._prefill}"
595
  )
596
  stream = self.llm.create_completion(
597
+ prompt, max_tokens=max_tokens, temperature=LOCAL_LLM_TEMPERATURE, top_p=0.8, top_k=20,
598
  presence_penalty=1.5, stop=["<|im_end|>", "<|endoftext|>"], stream=True,
599
  )
600
  read = lambda c: c["choices"][0].get("text") or ""
601
  else:
602
  stream = self.llm.create_chat_completion(
603
  messages=[{"role": "system", "content": system}, {"role": "user", "content": user}],
604
+ max_tokens=max_tokens, temperature=LOCAL_LLM_TEMPERATURE, top_p=0.8, stream=True,
605
  )
606
  read = lambda c: c["choices"][0].get("delta", {}).get("content") or ""
607
  pieces: List[str] = []
608
  truncated = False
609
  for chunk in stream:
610
  pieces.append(read(chunk))
611
+ if chunk["choices"][0].get("finish_reason") == "length":
612
+ truncated = True
613
  if time.monotonic() - started > LOCAL_LLM_TIME_BUDGET_S:
614
  truncated = True
615
  break
616
  import re
617
  text = re.sub(r"<think>.*?</think>", "", "".join(pieces), flags=re.S).strip()
618
  if truncated:
619
+ # Out of time or tokens: end on the last complete sentence. A dot
620
+ # after a digit is a list marker ("2."), not a sentence end.
621
+ ends = [m.end() for m in re.finditer(r"(?<!\d)[.!?][*_`\")\]]*(?=\s|$)", text)]
622
+ if ends and ends[-1] > len(text) * 0.4:
623
+ text = text[:ends[-1]].strip()
624
  if not text:
625
  return {**base, "response": None, "error": "local LLM returned no text"}
626
  return {
 
815
  logger.warning(f"Hosted LLM error: {type(e).__name__}: {str(e)[:160]} — using the local model")
816
  return None
817
 
818
+ # The in-Space model is small and slow: ask for a short answer it can finish
819
+ # inside the time budget, and tell it not to fill gaps with invented detail.
820
+ LOCAL_SYSTEM_PROMPT = (
821
+ "You are CyberForge AI, a cybersecurity analyst. Answer in at most 100 words, "
822
+ "as short bullet points. When assessing a threat, start with "
823
+ "'Risk Level: Critical, High, Medium or Low'. State only what you are sure of: "
824
+ "never invent CVE numbers, MITRE ATT&CK IDs, commands or reputation data, and "
825
+ "say so plainly when you do not know."
826
+ )
827
+
828
  def security_chat(self, query: str, max_tokens: int = 512, allow_local: bool = True) -> Dict:
829
  """Cybersecurity Q&A.
830
  1. Hosted model through HF Inference Providers, while the account has credit.
 
836
  return hosted
837
  if not allow_local:
838
  return {"model": "security-llm", "response": None, "error": "hosted LLM unavailable"}
839
+ result = local_llm.chat(self.LOCAL_SYSTEM_PROMPT, query, max_tokens=max_tokens)
840
  if result.get("response"):
841
  self.last_engine = "local"
842
  return result
requirements.txt CHANGED
@@ -2,7 +2,9 @@
2
  # Combined Gradio UI + FastAPI REST API
3
 
4
  # Core ML
5
- scikit-learn>=1.5.0
 
 
6
  pandas>=2.2.0
7
  numpy>=2.0.0
8
  joblib>=1.4.0
 
2
  # Combined Gradio UI + FastAPI REST API
3
 
4
  # Core ML
5
+ # Pinned: the .pkl files in Che237/cyberforge-models were saved with 1.8.0 and the
6
+ # gradient-boosting ones do not load on 1.9+. Retrain and re-upload before raising it.
7
+ scikit-learn==1.8.0
8
  pandas>=2.2.0
9
  numpy>=2.0.0
10
  joblib>=1.4.0