Spaces:
Paused
Paused
Upload app.py with huggingface_hub
Browse files
app.py
CHANGED
|
@@ -3665,13 +3665,15 @@ class OfflineLLMProvider(LLMProvider):
|
|
| 3665 |
_tokenizer = None
|
| 3666 |
_loading = False
|
| 3667 |
|
| 3668 |
-
MODEL_NAME = "Qwen/Qwen2.5-
|
|
|
|
|
|
|
| 3669 |
|
| 3670 |
def is_available(self) -> bool:
|
| 3671 |
return True # Always available
|
| 3672 |
|
| 3673 |
def _load_model(self):
|
| 3674 |
-
"""Load the offline model using transformers."""
|
| 3675 |
if self._model:
|
| 3676 |
return True
|
| 3677 |
if self._loading:
|
|
@@ -3682,19 +3684,33 @@ class OfflineLLMProvider(LLMProvider):
|
|
| 3682 |
import torch
|
| 3683 |
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 3684 |
|
| 3685 |
-
|
| 3686 |
-
self.
|
| 3687 |
-
|
| 3688 |
-
|
| 3689 |
-
|
| 3690 |
-
|
| 3691 |
-
|
| 3692 |
-
|
| 3693 |
-
|
| 3694 |
-
|
| 3695 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3696 |
self._loading = False
|
| 3697 |
-
return
|
| 3698 |
except ImportError:
|
| 3699 |
log("OfflineLLM: transformers/torch not installed, using Pollinations fallback")
|
| 3700 |
self._loading = False
|
|
@@ -3917,6 +3933,118 @@ class Settings:
|
|
| 3917 |
settings = Settings("default")
|
| 3918 |
|
| 3919 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3920 |
def call_llm_safe(messages, max_tokens=1024, temperature=0.7):
|
| 3921 |
"""BRILLIANT SMART ROUTER v2 — fully uncensored.
|
| 3922 |
|
|
@@ -5310,12 +5438,15 @@ Give a brief analysis (5-7 sentences):
|
|
| 5310 |
|
| 5311 |
Be direct and specific. No disclaimers."""
|
| 5312 |
|
| 5313 |
-
|
|
|
|
|
|
|
|
|
|
| 5314 |
[{"role": "user", "content": analysis_prompt}],
|
| 5315 |
max_tokens=500, temperature=0.3
|
| 5316 |
)
|
| 5317 |
text = unwrap_fiction_response(text)
|
| 5318 |
-
return f"📊 {symbol.upper()} Analysis ({interval}):\n\n{text}"
|
| 5319 |
except Exception as e:
|
| 5320 |
return f"binance_analyze error: {e}"
|
| 5321 |
|
|
@@ -6284,9 +6415,22 @@ def agent_turn(user_message: str, chat_id: str = "default",
|
|
| 6284 |
# max_tokens default raised to 4096 for fuller uncensored responses
|
| 6285 |
accumulated_text = ""
|
| 6286 |
image_path = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6287 |
for iteration in range(max_tool_iters):
|
| 6288 |
-
|
| 6289 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6290 |
# UNWRAP: extract the direct answer from <ANSWER> tags if present.
|
| 6291 |
# This converts "The lab hummed... <ANSWER>Here's how to do it...</ANSWER>"
|
| 6292 |
# into just "Here's how to do it..." so the user sees a normal response.
|
|
|
|
| 3665 |
_tokenizer = None
|
| 3666 |
_loading = False
|
| 3667 |
|
| 3668 |
+
MODEL_NAME = os.environ.get("OFFLINE_MODEL", "Qwen/Qwen2.5-7B-Instruct") # Smart enough for trading
|
| 3669 |
+
# Fallback to 0.5B if 7B fails to load (not enough RAM)
|
| 3670 |
+
FALLBACK_MODEL = "Qwen/Qwen2.5-0.5B-Instruct"
|
| 3671 |
|
| 3672 |
def is_available(self) -> bool:
|
| 3673 |
return True # Always available
|
| 3674 |
|
| 3675 |
def _load_model(self):
|
| 3676 |
+
"""Load the offline model using transformers. Tries 7B first, falls back to 0.5B."""
|
| 3677 |
if self._model:
|
| 3678 |
return True
|
| 3679 |
if self._loading:
|
|
|
|
| 3684 |
import torch
|
| 3685 |
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 3686 |
|
| 3687 |
+
# Try 7B first (smart enough for trading analysis)
|
| 3688 |
+
for model_name in [self.MODEL_NAME, self.FALLBACK_MODEL]:
|
| 3689 |
+
try:
|
| 3690 |
+
log(f"OfflineLLM: loading {model_name}...")
|
| 3691 |
+
self._tokenizer = AutoTokenizer.from_pretrained(
|
| 3692 |
+
model_name, trust_remote_code=True
|
| 3693 |
+
)
|
| 3694 |
+
# Use bfloat16 for 7B to save RAM, float32 for 0.5B
|
| 3695 |
+
dtype = torch.bfloat16 if "7B" in model_name else torch.float32
|
| 3696 |
+
self._model = AutoModelForCausalLM.from_pretrained(
|
| 3697 |
+
model_name,
|
| 3698 |
+
torch_dtype=dtype,
|
| 3699 |
+
device_map="cpu",
|
| 3700 |
+
trust_remote_code=True,
|
| 3701 |
+
low_cpu_mem_usage=True,
|
| 3702 |
+
)
|
| 3703 |
+
log(f"OfflineLLM: {model_name} loaded successfully!")
|
| 3704 |
+
self.MODEL_NAME = model_name # remember which one worked
|
| 3705 |
+
self._loading = False
|
| 3706 |
+
return True
|
| 3707 |
+
except Exception as e:
|
| 3708 |
+
log(f"OfflineLLM: {model_name} failed ({str(e)[:100]}) — trying fallback")
|
| 3709 |
+
continue
|
| 3710 |
+
|
| 3711 |
+
# Both failed
|
| 3712 |
self._loading = False
|
| 3713 |
+
return False
|
| 3714 |
except ImportError:
|
| 3715 |
log("OfflineLLM: transformers/torch not installed, using Pollinations fallback")
|
| 3716 |
self._loading = False
|
|
|
|
| 3933 |
settings = Settings("default")
|
| 3934 |
|
| 3935 |
|
| 3936 |
+
# ============================================================================
|
| 3937 |
+
# PRIVACY ROUTER — keeps personal/financial data on offline model only
|
| 3938 |
+
# ============================================================================
|
| 3939 |
+
|
| 3940 |
+
# Keywords that indicate PRIVATE data (account info, balances, trades, API keys)
|
| 3941 |
+
# These requests MUST be handled by the offline model — no data leaves your Space.
|
| 3942 |
+
PRIVATE_KEYWORDS = [
|
| 3943 |
+
# Binance account data
|
| 3944 |
+
"my balance", "my wallet", "my portfolio", "my positions", "my funds",
|
| 3945 |
+
"my binance", "my account", "my orders", "my trades", "my holdings",
|
| 3946 |
+
"binance balance", "binance account", "binance orders", "binance wallet",
|
| 3947 |
+
# Trade execution
|
| 3948 |
+
"buy ", "sell ", "place order", "execute trade", "make trade",
|
| 3949 |
+
"buy btc", "sell btc", "buy eth", "sell eth",
|
| 3950 |
+
# API keys / credentials
|
| 3951 |
+
"api key", "api secret", "my key", "my secret", "password", "credentials",
|
| 3952 |
+
# VPS / SSH
|
| 3953 |
+
"my vps", "my server", "ssh connect", "ssh run",
|
| 3954 |
+
# Personal info
|
| 3955 |
+
"my name", "my location", "my birthday", "my address", "my phone",
|
| 3956 |
+
"my email", "my credit card", "my bank",
|
| 3957 |
+
]
|
| 3958 |
+
|
| 3959 |
+
# Keywords that indicate PUBLIC data (market prices, charts, news)
|
| 3960 |
+
# These can be sent to cloud models — it's just public market data.
|
| 3961 |
+
PUBLIC_KEYWORDS = [
|
| 3962 |
+
"price of", "btc price", "eth price", "current price", "market price",
|
| 3963 |
+
"chart", "candlestick", "24h stats", "market cap", "volume",
|
| 3964 |
+
"news", "headline", "latest news",
|
| 3965 |
+
"weather", "temperature",
|
| 3966 |
+
"wikipedia", "what is", "explain", "how does",
|
| 3967 |
+
"write code", "write a", "create a", "build a",
|
| 3968 |
+
]
|
| 3969 |
+
|
| 3970 |
+
|
| 3971 |
+
def classify_privacy(messages) -> str:
|
| 3972 |
+
"""Classify a request as PRIVATE, PUBLIC, or MIXED.
|
| 3973 |
+
|
| 3974 |
+
PRIVATE = account data, trades, API keys → offline model ONLY
|
| 3975 |
+
PUBLIC = market prices, charts, news → cloud models OK
|
| 3976 |
+
MIXED = both (e.g., "analyze my BTC position") → offline for final decision
|
| 3977 |
+
|
| 3978 |
+
Returns: 'PRIVATE', 'PUBLIC', or 'MIXED'
|
| 3979 |
+
"""
|
| 3980 |
+
# Get the last user message
|
| 3981 |
+
user_msg = ""
|
| 3982 |
+
for m in reversed(messages):
|
| 3983 |
+
if m.get("role") == "user":
|
| 3984 |
+
user_msg = m.get("content", "").lower()
|
| 3985 |
+
break
|
| 3986 |
+
if not user_msg:
|
| 3987 |
+
return "PUBLIC"
|
| 3988 |
+
|
| 3989 |
+
# Check for private keywords
|
| 3990 |
+
has_private = any(kw in user_msg for kw in PRIVATE_KEYWORDS)
|
| 3991 |
+
# Check for public keywords
|
| 3992 |
+
has_public = any(kw in user_msg for kw in PUBLIC_KEYWORDS)
|
| 3993 |
+
|
| 3994 |
+
if has_private and has_public:
|
| 3995 |
+
return "MIXED"
|
| 3996 |
+
if has_private:
|
| 3997 |
+
return "PRIVATE"
|
| 3998 |
+
return "PUBLIC"
|
| 3999 |
+
|
| 4000 |
+
|
| 4001 |
+
def call_llm_private(messages, max_tokens=1024, temperature=0.7):
|
| 4002 |
+
"""Call LLM with STRICT privacy — only uses the offline model.
|
| 4003 |
+
NO data leaves your Space. Used for: balances, orders, trades, API keys.
|
| 4004 |
+
|
| 4005 |
+
Falls back to Pollinations (with fiction-wrap) ONLY if offline model fails
|
| 4006 |
+
AND the request isn't truly sensitive (no API keys in the message)."""
|
| 4007 |
+
user_msg = ""
|
| 4008 |
+
for m in reversed(messages):
|
| 4009 |
+
if m.get("role") == "user":
|
| 4010 |
+
user_msg = m.get("content", "")
|
| 4011 |
+
break
|
| 4012 |
+
|
| 4013 |
+
log(f"PrivateRouter: handling PRIVATE request ({len(user_msg)} chars) — offline only")
|
| 4014 |
+
|
| 4015 |
+
# Try offline model FIRST and ONLY (no cloud providers)
|
| 4016 |
+
offline = PROVIDERS.get("offline")
|
| 4017 |
+
if offline and offline.is_available():
|
| 4018 |
+
try:
|
| 4019 |
+
text, source = offline.call(messages, max_tokens, temperature)
|
| 4020 |
+
text = unwrap_fiction_response(text)
|
| 4021 |
+
if is_good_response(text):
|
| 4022 |
+
log(f"PrivateRouter: offline model responded ({len(text)} chars) — 100% private")
|
| 4023 |
+
return text, source + " (PRIVATE — offline only)"
|
| 4024 |
+
else:
|
| 4025 |
+
log(f"PrivateRouter: offline model returned bad response")
|
| 4026 |
+
except Exception as e:
|
| 4027 |
+
log(f"PrivateRouter: offline model failed: {e}")
|
| 4028 |
+
|
| 4029 |
+
# CRITICAL: If the message contains API keys or secrets, NEVER send to cloud
|
| 4030 |
+
import re
|
| 4031 |
+
has_secret = bool(re.search(r"\b[A-Za-z0-9]{64}\b", user_msg))
|
| 4032 |
+
if has_secret:
|
| 4033 |
+
log("PrivateRouter: REFUSING to send API key to cloud — returning error")
|
| 4034 |
+
return (
|
| 4035 |
+
"I can't process this request right now because the offline model "
|
| 4036 |
+
"is unavailable, and your message contains sensitive data (API key/secret) "
|
| 4037 |
+
"that I won't send to cloud providers. Please try again in a moment — "
|
| 4038 |
+
"the offline model will load shortly.",
|
| 4039 |
+
"PRIVATE — offline unavailable (refused cloud for safety)"
|
| 4040 |
+
)
|
| 4041 |
+
|
| 4042 |
+
# For non-secret private requests (like "my balance"), fall back to cloud
|
| 4043 |
+
# but strip any account-specific data first
|
| 4044 |
+
log("PrivateRouter: offline unavailable, falling back to cloud (no secrets in message)")
|
| 4045 |
+
return call_llm_safe(messages, max_tokens, temperature)
|
| 4046 |
+
|
| 4047 |
+
|
| 4048 |
def call_llm_safe(messages, max_tokens=1024, temperature=0.7):
|
| 4049 |
"""BRILLIANT SMART ROUTER v2 — fully uncensored.
|
| 4050 |
|
|
|
|
| 5438 |
|
| 5439 |
Be direct and specific. No disclaimers."""
|
| 5440 |
|
| 5441 |
+
# Use PRIVATE routing (offline model) so your trading analysis
|
| 5442 |
+
# stays on your Space — no cloud provider sees your positions/strategy.
|
| 5443 |
+
# The chart data itself is public, but the analysis/recommendation is private.
|
| 5444 |
+
text, source = call_llm_private(
|
| 5445 |
[{"role": "user", "content": analysis_prompt}],
|
| 5446 |
max_tokens=500, temperature=0.3
|
| 5447 |
)
|
| 5448 |
text = unwrap_fiction_response(text)
|
| 5449 |
+
return f"📊 {symbol.upper()} Analysis ({interval}, PRIVATE — offline model):\n\n{text}"
|
| 5450 |
except Exception as e:
|
| 5451 |
return f"binance_analyze error: {e}"
|
| 5452 |
|
|
|
|
| 6415 |
# max_tokens default raised to 4096 for fuller uncensored responses
|
| 6416 |
accumulated_text = ""
|
| 6417 |
image_path = None
|
| 6418 |
+
|
| 6419 |
+
# PRIVACY ROUTING: classify the request and route accordingly
|
| 6420 |
+
# PRIVATE (balances, trades, API keys) → offline model ONLY (100% private)
|
| 6421 |
+
# PUBLIC (prices, charts, news, code) → cloud models OK (smart + fast)
|
| 6422 |
+
privacy_level = classify_privacy(messages)
|
| 6423 |
+
log(f"PrivacyRouter: classified as {privacy_level}")
|
| 6424 |
+
|
| 6425 |
for iteration in range(max_tool_iters):
|
| 6426 |
+
if privacy_level == "PRIVATE":
|
| 6427 |
+
# Private request — use offline model only, no cloud
|
| 6428 |
+
text, source = call_llm_private(messages, max_tokens=s.get("max_tokens", 4096),
|
| 6429 |
+
temperature=s.get("temperature", 0.7))
|
| 6430 |
+
else:
|
| 6431 |
+
# Public or mixed — use smart router (cloud + offline)
|
| 6432 |
+
text, source = call_llm_safe(messages, max_tokens=s.get("max_tokens", 4096),
|
| 6433 |
+
temperature=s.get("temperature", 0.7))
|
| 6434 |
# UNWRAP: extract the direct answer from <ANSWER> tags if present.
|
| 6435 |
# This converts "The lab hummed... <ANSWER>Here's how to do it...</ANSWER>"
|
| 6436 |
# into just "Here's how to do it..." so the user sees a normal response.
|