Spaces:
Sleeping
Sleeping
Replace Gemini with in-Space open-source LLM (Qwen3.5-2B via llama.cpp)
Browse files- app.py +23 -9
- 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 |
-
|
| 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=
|
| 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=
|
| 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
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
|
|
|
| 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.
|
| 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 |
-
|
|
|
|
|
|
|
| 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
|