Spaces:
Running
Running
Replace HuggingChat with HuggingFace Widget Provider
Browse files- Add new HuggingFace Widget provider using mini chat interface
- Persistent browser session (no restart between requests)
- Top 10 models as Tier 1 priority:
* MiniMaxAI/MiniMax-M2.5
* moonshotai/Kimi-K2.5
* zai-org/GLM-5
* meta-llama/Llama-4-Scout/Maverick
* meta-llama/Llama-3.3-70B
* deepseek-ai/DeepSeek-V3
* Qwen/Qwen3-32B & Qwen2.5-72B
* microsoft/Phi-4
- Faster response times by reusing browser context
- Same HF credentials (one@bo5.store)
- config.py +29 -5
- engine.py +5 -5
- providers/huggingface_widget_provider.py +418 -0
config.py
CHANGED
|
@@ -14,25 +14,37 @@ Exhaustively tries ALL combinations before giving up.
|
|
| 14 |
# Examples: huggingchat-llama-3.3-70b, zai-glm-5, g4f-gpt-4, gemini-gemini-3-flash
|
| 15 |
# -------------------------------------------------------------------
|
| 16 |
MODEL_RANKING = [
|
| 17 |
-
# Tier 1 —
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
("g4f-gpt-4", "g4f", "gpt-4"),
|
| 19 |
("g4f-gpt-4o-mini", "g4f", "gpt-4o-mini"),
|
| 20 |
("zai-glm-5", "zai", "glm-5"),
|
| 21 |
("gemini-gemini-3-flash", "gemini", "gemini-3-flash"),
|
| 22 |
|
| 23 |
-
# Tier
|
| 24 |
("pollinations-gpt-oss-20b", "pollinations", "openai"),
|
| 25 |
("pollinations-mistral-small-3.2", "pollinations", "mistral"),
|
| 26 |
("pollinations-bidara", "pollinations", "bidara"),
|
| 27 |
("pollinations-chickytutor", "pollinations", "chickytutor"),
|
| 28 |
("pollinations-midijourney", "pollinations", "midijourney"),
|
| 29 |
|
| 30 |
-
# Tier
|
| 31 |
("g4f-gpt-3.5-turbo", "g4f", "gpt-3.5-turbo"),
|
| 32 |
("g4f-claude-3-haiku", "g4f", "claude-3-haiku"),
|
| 33 |
("g4f-mixtral-8x7b", "g4f", "mixtral-8x7b"),
|
| 34 |
|
| 35 |
-
# Tier
|
| 36 |
("opencode-kimi-k2.5-free", "opencode", "kimi-k2.5-free"),
|
| 37 |
("opencode-minimax-m2.5-free", "opencode", "minimax-m2.5-free"),
|
| 38 |
("opencode-big-pickle", "opencode", "big-pickle"),
|
|
@@ -75,7 +87,7 @@ PROVIDERS = {
|
|
| 75 |
"zai": {"enabled": True, "name": "Z.ai (GLM-5)", "type": "api"},
|
| 76 |
"gemini": {"enabled": True, "name": "Google Gemini", "type": "api"},
|
| 77 |
"pollinations": {"enabled": True, "name": "Pollinations", "type": "api"},
|
| 78 |
-
"
|
| 79 |
"copilot": {"enabled": False, "name": "Microsoft Copilot", "type": "browser"},
|
| 80 |
"chatgpt": {"enabled": False, "name": "ChatGPT", "type": "browser"},
|
| 81 |
"opencode": {"enabled": False, "name": "OpenCode Terminal", "type": "terminal"},
|
|
@@ -87,6 +99,18 @@ DEMO_API_KEY = "sk-kai-demo-public"
|
|
| 87 |
# Models per provider (for /models endpoint)
|
| 88 |
# All names follow the pattern: {provider}-{model-name}
|
| 89 |
PROVIDER_MODELS = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
"g4f": [
|
| 91 |
"g4f-gpt-4",
|
| 92 |
"g4f-gpt-4o-mini",
|
|
|
|
| 14 |
# Examples: huggingchat-llama-3.3-70b, zai-glm-5, g4f-gpt-4, gemini-gemini-3-flash
|
| 15 |
# -------------------------------------------------------------------
|
| 16 |
MODEL_RANKING = [
|
| 17 |
+
# Tier 1 — Top Hugging Face Models (Best Quality via Widget)
|
| 18 |
+
("hf-kimi-k2.5", "huggingface_widget", "hf-kimi-k2.5"),
|
| 19 |
+
("hf-minimax-m2.5", "huggingface_widget", "hf-minimax-m2.5"),
|
| 20 |
+
("hf-glm-5", "huggingface_widget", "hf-glm-5"),
|
| 21 |
+
("hf-llama-4-scout", "huggingface_widget", "hf-llama-4-scout"),
|
| 22 |
+
("hf-llama-4-maverick", "huggingface_widget", "hf-llama-4-maverick"),
|
| 23 |
+
("hf-llama-3.3-70b", "huggingface_widget", "hf-llama-3.3-70b"),
|
| 24 |
+
("hf-deepseek-v3", "huggingface_widget", "hf-deepseek-v3"),
|
| 25 |
+
("hf-qwen3-32b", "huggingface_widget", "hf-qwen3-32b"),
|
| 26 |
+
("hf-qwen2.5-72b", "huggingface_widget", "hf-qwen2.5-72b"),
|
| 27 |
+
("hf-phi-4", "huggingface_widget", "hf-phi-4"),
|
| 28 |
+
|
| 29 |
+
# Tier 2 — Other Providers
|
| 30 |
("g4f-gpt-4", "g4f", "gpt-4"),
|
| 31 |
("g4f-gpt-4o-mini", "g4f", "gpt-4o-mini"),
|
| 32 |
("zai-glm-5", "zai", "glm-5"),
|
| 33 |
("gemini-gemini-3-flash", "gemini", "gemini-3-flash"),
|
| 34 |
|
| 35 |
+
# Tier 3 — Pollinations
|
| 36 |
("pollinations-gpt-oss-20b", "pollinations", "openai"),
|
| 37 |
("pollinations-mistral-small-3.2", "pollinations", "mistral"),
|
| 38 |
("pollinations-bidara", "pollinations", "bidara"),
|
| 39 |
("pollinations-chickytutor", "pollinations", "chickytutor"),
|
| 40 |
("pollinations-midijourney", "pollinations", "midijourney"),
|
| 41 |
|
| 42 |
+
# Tier 4 — G4F Fallback Models
|
| 43 |
("g4f-gpt-3.5-turbo", "g4f", "gpt-3.5-turbo"),
|
| 44 |
("g4f-claude-3-haiku", "g4f", "claude-3-haiku"),
|
| 45 |
("g4f-mixtral-8x7b", "g4f", "mixtral-8x7b"),
|
| 46 |
|
| 47 |
+
# Tier 5 — OpenCode Terminal Models (Free)
|
| 48 |
("opencode-kimi-k2.5-free", "opencode", "kimi-k2.5-free"),
|
| 49 |
("opencode-minimax-m2.5-free", "opencode", "minimax-m2.5-free"),
|
| 50 |
("opencode-big-pickle", "opencode", "big-pickle"),
|
|
|
|
| 87 |
"zai": {"enabled": True, "name": "Z.ai (GLM-5)", "type": "api"},
|
| 88 |
"gemini": {"enabled": True, "name": "Google Gemini", "type": "api"},
|
| 89 |
"pollinations": {"enabled": True, "name": "Pollinations", "type": "api"},
|
| 90 |
+
"huggingface_widget": {"enabled": True, "name": "Hugging Face Widget", "type": "browser"},
|
| 91 |
"copilot": {"enabled": False, "name": "Microsoft Copilot", "type": "browser"},
|
| 92 |
"chatgpt": {"enabled": False, "name": "ChatGPT", "type": "browser"},
|
| 93 |
"opencode": {"enabled": False, "name": "OpenCode Terminal", "type": "terminal"},
|
|
|
|
| 99 |
# Models per provider (for /models endpoint)
|
| 100 |
# All names follow the pattern: {provider}-{model-name}
|
| 101 |
PROVIDER_MODELS = {
|
| 102 |
+
"huggingface_widget": [
|
| 103 |
+
"hf-kimi-k2.5",
|
| 104 |
+
"hf-minimax-m2.5",
|
| 105 |
+
"hf-glm-5",
|
| 106 |
+
"hf-llama-4-scout",
|
| 107 |
+
"hf-llama-4-maverick",
|
| 108 |
+
"hf-llama-3.3-70b",
|
| 109 |
+
"hf-deepseek-v3",
|
| 110 |
+
"hf-qwen3-32b",
|
| 111 |
+
"hf-qwen2.5-72b",
|
| 112 |
+
"hf-phi-4",
|
| 113 |
+
],
|
| 114 |
"g4f": [
|
| 115 |
"g4f-gpt-4",
|
| 116 |
"g4f-gpt-4o-mini",
|
engine.py
CHANGED
|
@@ -19,7 +19,7 @@ from providers.g4f_provider import G4FProvider
|
|
| 19 |
from providers.pollinations_provider import PollinationsProvider
|
| 20 |
from providers.gemini_provider import GeminiProvider
|
| 21 |
from providers.zai_provider import ZaiProvider
|
| 22 |
-
from providers.
|
| 23 |
from providers.copilot_provider import CopilotProvider
|
| 24 |
from providers.opencode_provider import OpenCodeProvider
|
| 25 |
from config import MODEL_RANKING, PROVIDER_MODELS, SUPABASE_URL, SUPABASE_KEY
|
|
@@ -61,15 +61,15 @@ class AIEngine:
|
|
| 61 |
self._providers["gemini"] = GeminiProvider()
|
| 62 |
logger.info("✅ Gemini provider enabled")
|
| 63 |
|
| 64 |
-
#
|
| 65 |
-
self._providers["
|
| 66 |
-
logger.info("✅
|
| 67 |
|
| 68 |
# Copilot also uses Playwright (with CAPTCHA support)
|
| 69 |
self._providers["copilot"] = CopilotProvider()
|
| 70 |
logger.info("✅ Copilot provider enabled (with CAPTCHA support)")
|
| 71 |
else:
|
| 72 |
-
logger.warning("⚠️ Z.ai/Gemini/
|
| 73 |
# Success Tracker: Key = "provider/model_id"
|
| 74 |
# Value = {success, failure, consecutive_failures, avg_time_ms, total_time_ms, count_samples}
|
| 75 |
self._stats: dict[str, dict] = {}
|
|
|
|
| 19 |
from providers.pollinations_provider import PollinationsProvider
|
| 20 |
from providers.gemini_provider import GeminiProvider
|
| 21 |
from providers.zai_provider import ZaiProvider
|
| 22 |
+
from providers.huggingface_widget_provider import HuggingFaceWidgetProvider
|
| 23 |
from providers.copilot_provider import CopilotProvider
|
| 24 |
from providers.opencode_provider import OpenCodeProvider
|
| 25 |
from config import MODEL_RANKING, PROVIDER_MODELS, SUPABASE_URL, SUPABASE_KEY
|
|
|
|
| 61 |
self._providers["gemini"] = GeminiProvider()
|
| 62 |
logger.info("✅ Gemini provider enabled")
|
| 63 |
|
| 64 |
+
# HuggingFace Widget also uses Playwright
|
| 65 |
+
self._providers["huggingface_widget"] = HuggingFaceWidgetProvider()
|
| 66 |
+
logger.info("✅ HuggingFace Widget provider enabled")
|
| 67 |
|
| 68 |
# Copilot also uses Playwright (with CAPTCHA support)
|
| 69 |
self._providers["copilot"] = CopilotProvider()
|
| 70 |
logger.info("✅ Copilot provider enabled (with CAPTCHA support)")
|
| 71 |
else:
|
| 72 |
+
logger.warning("⚠️ Z.ai/Gemini/HuggingFace Widget/Copilot providers disabled (Playwright not installed)")
|
| 73 |
# Success Tracker: Key = "provider/model_id"
|
| 74 |
# Value = {success, failure, consecutive_failures, avg_time_ms, total_time_ms, count_samples}
|
| 75 |
self._stats: dict[str, dict] = {}
|
providers/huggingface_widget_provider.py
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Hugging Face Widget Provider (Mini Chat)
|
| 3 |
+
----------------------------------------
|
| 4 |
+
Uses Playwright to interact with the mini chat widget on Hugging Face model pages.
|
| 5 |
+
Much faster than HuggingChat as it uses the embedded inference widget.
|
| 6 |
+
|
| 7 |
+
Strategy:
|
| 8 |
+
- Single persistent browser instance
|
| 9 |
+
- Navigate to model page and use the mini chat widget
|
| 10 |
+
- Start new chat by clearing/refreshing the widget
|
| 11 |
+
- Supports 10+ popular models
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
import asyncio
|
| 15 |
+
import logging
|
| 16 |
+
import re
|
| 17 |
+
from typing import Optional
|
| 18 |
+
from providers.base import BaseProvider
|
| 19 |
+
from config import PROVIDER_MODELS
|
| 20 |
+
|
| 21 |
+
logger = logging.getLogger("kai_api.huggingface_widget")
|
| 22 |
+
|
| 23 |
+
_playwright = None
|
| 24 |
+
_browser = None
|
| 25 |
+
_context = None
|
| 26 |
+
_lock = asyncio.Lock()
|
| 27 |
+
|
| 28 |
+
# Hugging Face credentials (same as HuggingChat)
|
| 29 |
+
HF_USERNAME = "one@bo5.store"
|
| 30 |
+
HF_PASSWORD = "Zzzzz1$."
|
| 31 |
+
|
| 32 |
+
# Top 10+ Popular models with their HF paths
|
| 33 |
+
POPULAR_MODELS = {
|
| 34 |
+
# Tier 1 - Most Popular
|
| 35 |
+
"hf-kimi-k2.5": "moonshotai/Kimi-K2.5",
|
| 36 |
+
"hf-minimax-m2.5": "MiniMaxAI/MiniMax-M2.5",
|
| 37 |
+
"hf-glm-5": "zai-org/GLM-5",
|
| 38 |
+
"hf-llama-4-scout": "meta-llama/Llama-4-Scout-17B-16E-Instruct",
|
| 39 |
+
"hf-llama-4-maverick": "meta-llama/Llama-4-Maverick-17B-128E-Instruct",
|
| 40 |
+
"hf-llama-3.3-70b": "meta-llama/Llama-3.3-70B-Instruct",
|
| 41 |
+
"hf-deepseek-v3": "deepseek-ai/DeepSeek-V3",
|
| 42 |
+
"hf-qwen3-32b": "Qwen/Qwen3-32B",
|
| 43 |
+
"hf-qwen2.5-72b": "Qwen/Qwen2.5-72B-Instruct",
|
| 44 |
+
"hf-phi-4": "microsoft/Phi-4",
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class HuggingFaceWidgetProvider(BaseProvider):
|
| 49 |
+
"""AI provider using Hugging Face model mini chat widgets."""
|
| 50 |
+
|
| 51 |
+
RESPONSE_TIMEOUT = 60
|
| 52 |
+
HYDRATION_DELAY = 1.5
|
| 53 |
+
|
| 54 |
+
@property
|
| 55 |
+
def name(self) -> str:
|
| 56 |
+
return "huggingface_widget"
|
| 57 |
+
|
| 58 |
+
def get_available_models(self) -> list[str]:
|
| 59 |
+
return list(POPULAR_MODELS.keys())
|
| 60 |
+
|
| 61 |
+
@staticmethod
|
| 62 |
+
def is_available() -> bool:
|
| 63 |
+
"""Check if Playwright is installed."""
|
| 64 |
+
try:
|
| 65 |
+
from playwright.async_api import async_playwright
|
| 66 |
+
return True
|
| 67 |
+
except ImportError:
|
| 68 |
+
return False
|
| 69 |
+
|
| 70 |
+
async def _ensure_browser(self):
|
| 71 |
+
"""Start persistent browser and context if not running."""
|
| 72 |
+
global _playwright, _browser, _context
|
| 73 |
+
|
| 74 |
+
async with _lock:
|
| 75 |
+
if _browser and _browser.is_connected():
|
| 76 |
+
return
|
| 77 |
+
|
| 78 |
+
logger.info("🚀 HuggingFace Widget: Launching browser...")
|
| 79 |
+
from playwright.async_api import async_playwright
|
| 80 |
+
|
| 81 |
+
_playwright = await async_playwright().start()
|
| 82 |
+
_browser = await _playwright.chromium.launch(
|
| 83 |
+
headless=True,
|
| 84 |
+
args=[
|
| 85 |
+
"--disable-blink-features=AutomationControlled",
|
| 86 |
+
"--no-sandbox",
|
| 87 |
+
"--disable-dev-shm-usage",
|
| 88 |
+
"--disable-gpu",
|
| 89 |
+
],
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
# Create persistent context (cookies persist across requests)
|
| 93 |
+
_context = await _browser.new_context(
|
| 94 |
+
viewport={"width": 1920, "height": 1080},
|
| 95 |
+
user_agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
|
| 96 |
+
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
| 97 |
+
"Chrome/120.0.0.0 Safari/537.36",
|
| 98 |
+
locale="en-US",
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
# Hide webdriver
|
| 102 |
+
await _context.add_init_script("""
|
| 103 |
+
Object.defineProperty(navigator, 'webdriver', {get: () => undefined});
|
| 104 |
+
""")
|
| 105 |
+
|
| 106 |
+
logger.info("✅ HuggingFace Widget: Browser ready")
|
| 107 |
+
|
| 108 |
+
async def _ensure_logged_in(self):
|
| 109 |
+
"""Check if logged in, if not perform login."""
|
| 110 |
+
global _context
|
| 111 |
+
|
| 112 |
+
page = await _context.new_page()
|
| 113 |
+
try:
|
| 114 |
+
# Check if we're logged in by going to a model page
|
| 115 |
+
await page.goto("https://huggingface.co/meta-llama/Llama-3.3-70B-Instruct", timeout=30000)
|
| 116 |
+
await asyncio.sleep(1)
|
| 117 |
+
|
| 118 |
+
# Check for login button
|
| 119 |
+
login_btn = await page.query_selector('a[href*="login"], button:has-text("Log in")')
|
| 120 |
+
|
| 121 |
+
if login_btn:
|
| 122 |
+
logger.info("HF Widget: Not logged in, performing login...")
|
| 123 |
+
await self._perform_login()
|
| 124 |
+
else:
|
| 125 |
+
logger.info("HF Widget: Already logged in")
|
| 126 |
+
|
| 127 |
+
except Exception as e:
|
| 128 |
+
logger.warning(f"HF Widget: Login check failed: {e}")
|
| 129 |
+
finally:
|
| 130 |
+
await page.close()
|
| 131 |
+
|
| 132 |
+
async def _perform_login(self):
|
| 133 |
+
"""Login to Hugging Face."""
|
| 134 |
+
global _context
|
| 135 |
+
|
| 136 |
+
page = await _context.new_page()
|
| 137 |
+
try:
|
| 138 |
+
logger.info("HF Widget: Logging in...")
|
| 139 |
+
await page.goto("https://huggingface.co/login", timeout=60000)
|
| 140 |
+
|
| 141 |
+
# Fill credentials
|
| 142 |
+
await page.wait_for_selector('input[name="username"]', timeout=10000)
|
| 143 |
+
await page.fill('input[name="username"]', HF_USERNAME)
|
| 144 |
+
await asyncio.sleep(0.3)
|
| 145 |
+
await page.fill('input[name="password"]', HF_PASSWORD)
|
| 146 |
+
await asyncio.sleep(0.3)
|
| 147 |
+
|
| 148 |
+
# Submit
|
| 149 |
+
await page.click('button[type="submit"]')
|
| 150 |
+
|
| 151 |
+
# Wait for redirect
|
| 152 |
+
try:
|
| 153 |
+
await page.wait_for_url(lambda url: "login" not in url, timeout=15000)
|
| 154 |
+
logger.info("✅ HF Widget: Login successful")
|
| 155 |
+
except:
|
| 156 |
+
current_url = page.url
|
| 157 |
+
if "login" in current_url:
|
| 158 |
+
logger.error("❌ HF Widget: Login failed")
|
| 159 |
+
raise RuntimeError("Failed to login to Hugging Face")
|
| 160 |
+
|
| 161 |
+
finally:
|
| 162 |
+
await page.close()
|
| 163 |
+
|
| 164 |
+
async def send_message(
|
| 165 |
+
self,
|
| 166 |
+
prompt: str,
|
| 167 |
+
model: str | None = None,
|
| 168 |
+
system_prompt: str | None = None,
|
| 169 |
+
**kwargs,
|
| 170 |
+
) -> dict:
|
| 171 |
+
"""Send message via Hugging Face model widget."""
|
| 172 |
+
if not self.is_available():
|
| 173 |
+
raise RuntimeError("Playwright not installed")
|
| 174 |
+
|
| 175 |
+
await self._ensure_browser()
|
| 176 |
+
await self._ensure_logged_in()
|
| 177 |
+
|
| 178 |
+
global _context
|
| 179 |
+
|
| 180 |
+
# Get model path
|
| 181 |
+
selected_model = model or "hf-kimi-k2.5"
|
| 182 |
+
model_path = POPULAR_MODELS.get(selected_model, selected_model.replace("hf-", ""))
|
| 183 |
+
|
| 184 |
+
if selected_model not in POPULAR_MODELS:
|
| 185 |
+
selected_model = "hf-kimi-k2.5"
|
| 186 |
+
model_path = POPULAR_MODELS[selected_model]
|
| 187 |
+
|
| 188 |
+
logger.info(f"HF Widget request: {selected_model} ({model_path})")
|
| 189 |
+
|
| 190 |
+
page = await _context.new_page()
|
| 191 |
+
|
| 192 |
+
try:
|
| 193 |
+
# Navigate to model page
|
| 194 |
+
url = f"https://huggingface.co/{model_path}"
|
| 195 |
+
await page.goto(url, timeout=60000)
|
| 196 |
+
await asyncio.sleep(self.HYDRATION_DELAY)
|
| 197 |
+
|
| 198 |
+
# Handle cookie consent if present
|
| 199 |
+
try:
|
| 200 |
+
cookie_btn = await page.wait_for_selector(
|
| 201 |
+
'button:has-text("Accept"), button:has-text("I agree")',
|
| 202 |
+
timeout=3000
|
| 203 |
+
)
|
| 204 |
+
if cookie_btn:
|
| 205 |
+
await cookie_btn.click()
|
| 206 |
+
await asyncio.sleep(0.5)
|
| 207 |
+
except:
|
| 208 |
+
pass
|
| 209 |
+
|
| 210 |
+
# Find the mini chat widget input
|
| 211 |
+
# Try multiple selectors for different widget versions
|
| 212 |
+
input_selectors = [
|
| 213 |
+
'[data-target="WidgetChatInput"] textarea',
|
| 214 |
+
'.inference-widget textarea',
|
| 215 |
+
'[data-target="InferenceWidget"] textarea',
|
| 216 |
+
'textarea[placeholder*="chat"]',
|
| 217 |
+
'textarea[placeholder*="message"]',
|
| 218 |
+
'.widget-container textarea',
|
| 219 |
+
'[class*="chat-input"] textarea',
|
| 220 |
+
]
|
| 221 |
+
|
| 222 |
+
input_selector = None
|
| 223 |
+
for sel in input_selectors:
|
| 224 |
+
try:
|
| 225 |
+
el = await page.wait_for_selector(sel, timeout=2000)
|
| 226 |
+
if el:
|
| 227 |
+
input_selector = sel
|
| 228 |
+
logger.info(f"HF Widget: Found input using {sel}")
|
| 229 |
+
break
|
| 230 |
+
except:
|
| 231 |
+
continue
|
| 232 |
+
|
| 233 |
+
if not input_selector:
|
| 234 |
+
# Try to scroll to find the widget
|
| 235 |
+
await page.evaluate("window.scrollTo(0, document.body.scrollHeight * 0.3)")
|
| 236 |
+
await asyncio.sleep(1)
|
| 237 |
+
|
| 238 |
+
# Try again
|
| 239 |
+
for sel in input_selectors:
|
| 240 |
+
try:
|
| 241 |
+
el = await page.wait_for_selector(sel, timeout=3000)
|
| 242 |
+
if el:
|
| 243 |
+
input_selector = sel
|
| 244 |
+
break
|
| 245 |
+
except:
|
| 246 |
+
continue
|
| 247 |
+
|
| 248 |
+
if not input_selector:
|
| 249 |
+
raise RuntimeError("Could not find chat widget input")
|
| 250 |
+
|
| 251 |
+
# Clear any existing conversation (start fresh)
|
| 252 |
+
await self._clear_chat(page)
|
| 253 |
+
|
| 254 |
+
# Type message
|
| 255 |
+
full_prompt = prompt
|
| 256 |
+
if system_prompt:
|
| 257 |
+
full_prompt = f"[System: {system_prompt}]\n\n{prompt}"
|
| 258 |
+
|
| 259 |
+
await page.fill(input_selector, full_prompt)
|
| 260 |
+
await asyncio.sleep(0.3)
|
| 261 |
+
|
| 262 |
+
# Submit (usually Enter key)
|
| 263 |
+
await page.keyboard.press("Enter")
|
| 264 |
+
logger.info("HF Widget: Message sent, waiting for response...")
|
| 265 |
+
|
| 266 |
+
# Wait for response
|
| 267 |
+
response_text = await self._wait_for_response(page)
|
| 268 |
+
|
| 269 |
+
if not response_text:
|
| 270 |
+
raise ValueError("Empty response from model")
|
| 271 |
+
|
| 272 |
+
logger.info(f"HF Widget: Got response ({len(response_text)} chars)")
|
| 273 |
+
|
| 274 |
+
return {
|
| 275 |
+
"response": response_text,
|
| 276 |
+
"model": selected_model,
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
except Exception as e:
|
| 280 |
+
logger.error(f"HF Widget Error: {e}")
|
| 281 |
+
raise
|
| 282 |
+
finally:
|
| 283 |
+
await page.close()
|
| 284 |
+
|
| 285 |
+
async def _clear_chat(self, page):
|
| 286 |
+
"""Clear existing chat to start fresh conversation."""
|
| 287 |
+
try:
|
| 288 |
+
# Look for clear/new chat button
|
| 289 |
+
clear_selectors = [
|
| 290 |
+
'button:has-text("Clear")',
|
| 291 |
+
'button:has-text("New")',
|
| 292 |
+
'button:has-text("Reset")',
|
| 293 |
+
'[data-target="ClearChat"]',
|
| 294 |
+
'[class*="clear-chat"]',
|
| 295 |
+
]
|
| 296 |
+
|
| 297 |
+
for sel in clear_selectors:
|
| 298 |
+
try:
|
| 299 |
+
btn = await page.wait_for_selector(sel, timeout=2000)
|
| 300 |
+
if btn:
|
| 301 |
+
await btn.click()
|
| 302 |
+
logger.info("HF Widget: Cleared previous chat")
|
| 303 |
+
await asyncio.sleep(0.5)
|
| 304 |
+
return
|
| 305 |
+
except:
|
| 306 |
+
continue
|
| 307 |
+
|
| 308 |
+
# If no clear button, refresh the page to start fresh
|
| 309 |
+
logger.info("HF Widget: Refreshing page for new chat")
|
| 310 |
+
await page.reload()
|
| 311 |
+
await asyncio.sleep(1.5)
|
| 312 |
+
|
| 313 |
+
except Exception as e:
|
| 314 |
+
logger.warning(f"HF Widget: Could not clear chat: {e}")
|
| 315 |
+
|
| 316 |
+
async def _wait_for_response(self, page) -> str:
|
| 317 |
+
"""Wait for and extract response from widget."""
|
| 318 |
+
last_text = ""
|
| 319 |
+
stable_count = 0
|
| 320 |
+
required_stable = 2
|
| 321 |
+
|
| 322 |
+
for i in range(self.RESPONSE_TIMEOUT * 2):
|
| 323 |
+
await asyncio.sleep(0.5)
|
| 324 |
+
|
| 325 |
+
# Check if still loading/generating
|
| 326 |
+
is_loading = await page.evaluate("""
|
| 327 |
+
() => {
|
| 328 |
+
const loading = document.querySelectorAll(
|
| 329 |
+
'[class*="loading"], [class*="spinner"], [class*="animate-pulse"], ' +
|
| 330 |
+
'[data-loading="true"], .generating'
|
| 331 |
+
);
|
| 332 |
+
return loading.length > 0;
|
| 333 |
+
}
|
| 334 |
+
""")
|
| 335 |
+
|
| 336 |
+
if is_loading:
|
| 337 |
+
continue
|
| 338 |
+
|
| 339 |
+
# Extract response text
|
| 340 |
+
current_text = await page.evaluate("""
|
| 341 |
+
() => {
|
| 342 |
+
// Try different selectors for the assistant response
|
| 343 |
+
const selectors = [
|
| 344 |
+
'[data-target="WidgetMessage"][data-role="assistant"]',
|
| 345 |
+
'.widget-message.assistant',
|
| 346 |
+
'[data-role="assistant"] .message-content',
|
| 347 |
+
'.inference-widget [data-message-role="assistant"]',
|
| 348 |
+
'.chat-message.assistant',
|
| 349 |
+
'[class*="assistant"] [class*="content"]',
|
| 350 |
+
'.widget-container .response',
|
| 351 |
+
];
|
| 352 |
+
|
| 353 |
+
for (const sel of selectors) {
|
| 354 |
+
const els = document.querySelectorAll(sel);
|
| 355 |
+
if (els.length > 0) {
|
| 356 |
+
// Get the last response
|
| 357 |
+
const last = els[els.length - 1];
|
| 358 |
+
const text = last.innerText || last.textContent || '';
|
| 359 |
+
if (text.trim().length > 5) return text.trim();
|
| 360 |
+
}
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
// Fallback: look for any non-user message
|
| 364 |
+
const allMessages = document.querySelectorAll('.message, .chat-message, [class*="message"]');
|
| 365 |
+
for (const msg of allMessages) {
|
| 366 |
+
const isUser = msg.classList.contains('user') ||
|
| 367 |
+
msg.getAttribute('data-role') === 'user' ||
|
| 368 |
+
msg.querySelector('.user');
|
| 369 |
+
if (!isUser) {
|
| 370 |
+
const text = msg.innerText || msg.textContent || '';
|
| 371 |
+
if (text.trim().length > 10) return text.trim();
|
| 372 |
+
}
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
return '';
|
| 376 |
+
}
|
| 377 |
+
""")
|
| 378 |
+
|
| 379 |
+
if not current_text:
|
| 380 |
+
continue
|
| 381 |
+
|
| 382 |
+
clean = self._clean_response(current_text)
|
| 383 |
+
|
| 384 |
+
if clean == last_text and len(clean) > 10:
|
| 385 |
+
stable_count += 1
|
| 386 |
+
if stable_count >= required_stable:
|
| 387 |
+
return clean
|
| 388 |
+
else:
|
| 389 |
+
stable_count = 0
|
| 390 |
+
last_text = clean
|
| 391 |
+
|
| 392 |
+
if i % 10 == 9:
|
| 393 |
+
logger.info(f"HF Widget: Streaming... {len(last_text)} chars")
|
| 394 |
+
|
| 395 |
+
if last_text:
|
| 396 |
+
logger.warning("HF Widget: Timeout, returning partial response")
|
| 397 |
+
return last_text
|
| 398 |
+
|
| 399 |
+
raise TimeoutError("No response from model")
|
| 400 |
+
|
| 401 |
+
def _clean_response(self, text: str) -> str:
|
| 402 |
+
"""Clean up response text."""
|
| 403 |
+
clean = text.strip()
|
| 404 |
+
# Remove common artifacts
|
| 405 |
+
clean = re.sub(r"\n+\s*\n+", "\n\n", clean)
|
| 406 |
+
clean = re.sub(r"^User:\s*", "", clean, flags=re.IGNORECASE)
|
| 407 |
+
clean = re.sub(r"^Assistant:\s*", "", clean, flags=re.IGNORECASE)
|
| 408 |
+
return clean.strip()
|
| 409 |
+
|
| 410 |
+
async def health_check(self) -> bool:
|
| 411 |
+
"""Quick health check."""
|
| 412 |
+
try:
|
| 413 |
+
if not self.is_available():
|
| 414 |
+
return False
|
| 415 |
+
await self._ensure_browser()
|
| 416 |
+
return _browser.is_connected()
|
| 417 |
+
except Exception:
|
| 418 |
+
return False
|