Spaces:
Sleeping
Sleeping
Upload 4 files
Browse files- README.md +57 -0
- app.py +399 -0
- requirements.txt +6 -0
- storage.py +71 -0
README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Scraper Bot v3 — Multi-Turn Chat with Persistent Storage
|
| 3 |
+
emoji: 🧠
|
| 4 |
+
colorFrom: purple
|
| 5 |
+
colorTo: indigo
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: 4.0.0
|
| 8 |
+
app_file: app.py
|
| 9 |
+
pinned: false
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
# Version 3 — Multi-Turn AI Chatbot with Persistent Storage
|
| 13 |
+
|
| 14 |
+
AI assistant with **session and cross-session memory** and **editable user preferences**.
|
| 15 |
+
|
| 16 |
+
## Features
|
| 17 |
+
|
| 18 |
+
- **Multi-turn conversation (short-term / session memory)**
|
| 19 |
+
Conversation history is kept in the format `[{"role": "user"|"assistant", "content": "..."}]` and sent to the API on every turn so the model can answer follow-up questions (e.g. *"Tell me more about the second book"*).
|
| 20 |
+
|
| 21 |
+
- **Persistent storage (cross-session memory)**
|
| 22 |
+
A local JSON file (`chat_storage.json`) stores:
|
| 23 |
+
- Conversation history for both tabs (Website Scraper and YouTube Transcript).
|
| 24 |
+
- On startup, existing history is loaded and shown in the chat.
|
| 25 |
+
- After each turn, the updated history is written to the file.
|
| 26 |
+
|
| 27 |
+
- **Editable user preferences**
|
| 28 |
+
You can set preferences at run time (e.g. *"Always respond formally"*, *"Cite sources when summarizing"*). They are saved in the same JSON file and **injected into the system prompt on every API call**.
|
| 29 |
+
|
| 30 |
+
## Tabs
|
| 31 |
+
|
| 32 |
+
1. **Bot-Protected Website Scraper** — Enter a URL (e.g. Goodreads), scrape with Bright Data + BeautifulSoup, then chat with multi-turn memory.
|
| 33 |
+
2. **YouTube Transcript Q&A** — Enter a video ID or URL, fetch transcript with `youtube-transcript-api`, then chat with multi-turn memory.
|
| 34 |
+
|
| 35 |
+
## Hugging Face Spaces
|
| 36 |
+
|
| 37 |
+
1. Create a new Space, SDK **Gradio**.
|
| 38 |
+
2. Clone the repo and add `app.py`, `storage.py`, and `requirements.txt`.
|
| 39 |
+
3. In **Settings → Repository secrets** add:
|
| 40 |
+
- `GROQ_API_KEY`
|
| 41 |
+
- `BRIGHTDATA_API_KEY`
|
| 42 |
+
4. Push; the app will build and run.
|
| 43 |
+
Note: On Spaces, `chat_storage.json` is not persistent across restarts unless you use a persistent volume.
|
| 44 |
+
|
| 45 |
+
## Local run
|
| 46 |
+
|
| 47 |
+
```bash
|
| 48 |
+
pip install -r requirements.txt
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
Create a `.env` file with `GROQ_API_KEY` and `BRIGHTDATA_API_KEY`, then:
|
| 52 |
+
|
| 53 |
+
```bash
|
| 54 |
+
python app.py
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
Open `http://127.0.0.1:7862` (port 7862 to avoid conflict with v2).
|
app.py
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Version 3 — Multi-Turn AI Chatbot with Persistent Storage
|
| 3 |
+
|
| 4 |
+
- Multi-turn conversation: full history sent to API so the model can answer follow-ups.
|
| 5 |
+
- Persistent storage: chat_history.json holds conversation history and user preferences across restarts.
|
| 6 |
+
- Editable user preferences: injected into the system prompt on every API call.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import os
|
| 10 |
+
import re
|
| 11 |
+
import urllib3
|
| 12 |
+
from urllib.parse import urlparse
|
| 13 |
+
|
| 14 |
+
import gradio as gr
|
| 15 |
+
import requests
|
| 16 |
+
from bs4 import BeautifulSoup
|
| 17 |
+
from dotenv import load_dotenv
|
| 18 |
+
from groq import Groq
|
| 19 |
+
from youtube_transcript_api import YouTubeTranscriptApi
|
| 20 |
+
|
| 21 |
+
from storage import load_storage, save_storage
|
| 22 |
+
|
| 23 |
+
load_dotenv()
|
| 24 |
+
|
| 25 |
+
GROQ_API_KEY = os.getenv("GROQ_API_KEY")
|
| 26 |
+
BRIGHTDATA_API_KEY = os.getenv("BRIGHTDATA_API_KEY")
|
| 27 |
+
|
| 28 |
+
if not GROQ_API_KEY:
|
| 29 |
+
raise ValueError("GROQ_API_KEY is not set.")
|
| 30 |
+
|
| 31 |
+
client = Groq(api_key=GROQ_API_KEY)
|
| 32 |
+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
| 33 |
+
|
| 34 |
+
# In-memory: scraped/transcript context + loaded storage (updated on save)
|
| 35 |
+
contexts = {"scraper": "", "youtube": ""}
|
| 36 |
+
storage = load_storage()
|
| 37 |
+
# Do NOT clear history on startup; persist full conversation across restarts.
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
# ---------------------------------------------------------------------------
|
| 41 |
+
# Tab 1: Bot-Protected Website Scraper
|
| 42 |
+
# ---------------------------------------------------------------------------
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def scrape_website(url: str):
|
| 46 |
+
if not url:
|
| 47 |
+
return "Please enter a URL.", ""
|
| 48 |
+
|
| 49 |
+
parsed = urlparse(url)
|
| 50 |
+
target_url = url
|
| 51 |
+
if "goodreads.com" in (parsed.netloc or "") and (parsed.path in ("", "/")):
|
| 52 |
+
target_url = "https://www.goodreads.com/list/show/1.Best_Books_Ever"
|
| 53 |
+
|
| 54 |
+
api_url = "https://3.232.71.244/request"
|
| 55 |
+
headers = {
|
| 56 |
+
"Authorization": f"Bearer {BRIGHTDATA_API_KEY}",
|
| 57 |
+
"Content-Type": "application/json",
|
| 58 |
+
"Host": "api.brightdata.com",
|
| 59 |
+
}
|
| 60 |
+
payload = {"zone": "goodreads_unlocker", "url": target_url, "format": "raw"}
|
| 61 |
+
|
| 62 |
+
try:
|
| 63 |
+
resp = requests.post(
|
| 64 |
+
api_url, json=payload, headers=headers, timeout=120, verify=False
|
| 65 |
+
)
|
| 66 |
+
resp.raise_for_status()
|
| 67 |
+
soup = BeautifulSoup(resp.text, "html.parser")
|
| 68 |
+
|
| 69 |
+
if "goodreads.com/list/show/1.Best_Books_Ever" in target_url:
|
| 70 |
+
books_data = []
|
| 71 |
+
book_rows = soup.find_all("tr", itemtype="http://schema.org/Book")
|
| 72 |
+
for idx, row in enumerate(book_rows):
|
| 73 |
+
title_elem = row.find("a", class_="bookTitle")
|
| 74 |
+
author_elem = row.find("a", class_="authorName")
|
| 75 |
+
rating_elem = row.find("span", class_="minirating")
|
| 76 |
+
title = title_elem.text.strip() if title_elem else "Unknown Title"
|
| 77 |
+
author = author_elem.text.strip() if author_elem else "Unknown Author"
|
| 78 |
+
rating = rating_elem.text.strip() if rating_elem else "Unknown Rating"
|
| 79 |
+
books_data.append(
|
| 80 |
+
{"Rank": idx + 1, "Title": title, "Author": author, "Rating": rating}
|
| 81 |
+
)
|
| 82 |
+
if books_data:
|
| 83 |
+
lines = [
|
| 84 |
+
"Here is the scraped data from Goodreads Best Books Ever list:",
|
| 85 |
+
"",
|
| 86 |
+
]
|
| 87 |
+
for b in books_data:
|
| 88 |
+
lines.append(
|
| 89 |
+
f"{b['Rank']}. {b['Title']} by {b['Author']} - {b['Rating']}"
|
| 90 |
+
)
|
| 91 |
+
text_content = "\n".join(lines)
|
| 92 |
+
else:
|
| 93 |
+
text_content = soup.get_text(separator=" ", strip=True)
|
| 94 |
+
else:
|
| 95 |
+
text_content = soup.get_text(separator=" ", strip=True)
|
| 96 |
+
|
| 97 |
+
contexts["scraper"] = text_content[:15000]
|
| 98 |
+
preview = (
|
| 99 |
+
text_content[:500] + "..." if len(text_content) > 500 else text_content
|
| 100 |
+
)
|
| 101 |
+
return "Website scraped successfully. You can now chat about it.", preview
|
| 102 |
+
except Exception as e:
|
| 103 |
+
contexts["scraper"] = ""
|
| 104 |
+
return f"Error scraping website: {e}", ""
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
# ---------------------------------------------------------------------------
|
| 108 |
+
# Tab 2: YouTube Transcript Q&A
|
| 109 |
+
# ---------------------------------------------------------------------------
|
| 110 |
+
|
| 111 |
+
_YOUTUBE_ID_REGEX = re.compile(
|
| 112 |
+
r"(https?://)?(www\.)?"
|
| 113 |
+
r"(youtube|youtu|youtube-nocookie)\.(com|be)/"
|
| 114 |
+
r"(watch\?v=|embed/|v/|.+\?v=)?([^&=%\?]{11})"
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def _extract_video_id(url_or_id: str) -> str:
|
| 119 |
+
match = _YOUTUBE_ID_REGEX.search(url_or_id)
|
| 120 |
+
if match:
|
| 121 |
+
return match.group(6)
|
| 122 |
+
return url_or_id.strip()
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def fetch_youtube_transcript(video_input: str):
|
| 126 |
+
if not video_input:
|
| 127 |
+
return "Please enter a YouTube Video URL or ID.", ""
|
| 128 |
+
|
| 129 |
+
video_id = _extract_video_id(video_input)
|
| 130 |
+
try:
|
| 131 |
+
api = YouTubeTranscriptApi()
|
| 132 |
+
transcript_list = api.list(video_id)
|
| 133 |
+
transcript = None
|
| 134 |
+
try:
|
| 135 |
+
transcript = transcript_list.find_transcript(["en", "ur"])
|
| 136 |
+
except Exception:
|
| 137 |
+
try:
|
| 138 |
+
transcript = transcript_list.find_generated_transcript(["en", "ur"])
|
| 139 |
+
except Exception:
|
| 140 |
+
for t in transcript_list:
|
| 141 |
+
transcript = t
|
| 142 |
+
break
|
| 143 |
+
if transcript is None:
|
| 144 |
+
raise Exception("No transcript available for this video.")
|
| 145 |
+
transcript_data = transcript.fetch()
|
| 146 |
+
pieces = []
|
| 147 |
+
for t in transcript_data:
|
| 148 |
+
if isinstance(t, dict):
|
| 149 |
+
pieces.append(t.get("text", ""))
|
| 150 |
+
else:
|
| 151 |
+
pieces.append(getattr(t, "text", ""))
|
| 152 |
+
transcript_text = " ".join(pieces)
|
| 153 |
+
contexts["youtube"] = transcript_text[:15000]
|
| 154 |
+
preview = (
|
| 155 |
+
transcript_text[:500] + "..."
|
| 156 |
+
if len(transcript_text) > 500
|
| 157 |
+
else transcript_text
|
| 158 |
+
)
|
| 159 |
+
return (
|
| 160 |
+
"Transcript fetched successfully. You can now chat about the video.",
|
| 161 |
+
preview,
|
| 162 |
+
)
|
| 163 |
+
except Exception as e:
|
| 164 |
+
contexts["youtube"] = ""
|
| 165 |
+
return f"Error: No transcript for video ID ({video_id}). Details: {e}", ""
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
# ---------------------------------------------------------------------------
|
| 169 |
+
# Multi-turn chat with persistent storage and user preferences
|
| 170 |
+
# ---------------------------------------------------------------------------
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
def _build_system_prompt(mode: str) -> str:
|
| 174 |
+
context = contexts.get(mode, "")
|
| 175 |
+
prefs = storage.get("user_preferences", "").strip()
|
| 176 |
+
ctx_placeholder = "(None — the user has NOT scraped or fetched transcript yet. You must refuse to answer and tell them to scrape/fetch first.)"
|
| 177 |
+
base = (
|
| 178 |
+
"You are a helpful assistant. You must use ONLY the provided context to answer. "
|
| 179 |
+
"NEVER use external knowledge or general information. If the context says 'None' or the user has not scraped yet, refuse to answer and tell them to scrape the website or fetch the transcript first. "
|
| 180 |
+
"If the answer is not in the context, say so. You have conversation history for follow-up questions (e.g. 'tell me more about the second book').\n\n"
|
| 181 |
+
f"Context:\n{context.strip() if context else ctx_placeholder}"
|
| 182 |
+
)
|
| 183 |
+
if prefs:
|
| 184 |
+
base += f"\n\nUser preferences (follow these):\n{prefs}"
|
| 185 |
+
return base
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def chat_turn_scraper(message: str, _history_ignored):
|
| 189 |
+
"""One turn in scraper tab: use storage as source of truth, append message, call API with full history, persist, return new history."""
|
| 190 |
+
if not message or not message.strip():
|
| 191 |
+
return "", storage.get("scraper_history", [])
|
| 192 |
+
|
| 193 |
+
context = contexts.get("scraper", "") or ""
|
| 194 |
+
if not context.strip():
|
| 195 |
+
history_dicts = list(storage.get("scraper_history", []))
|
| 196 |
+
history_dicts.append({"role": "user", "content": message.strip()})
|
| 197 |
+
history_dicts.append(
|
| 198 |
+
{
|
| 199 |
+
"role": "assistant",
|
| 200 |
+
"content": "Please scrape a website first, then ask questions.",
|
| 201 |
+
}
|
| 202 |
+
)
|
| 203 |
+
storage["scraper_history"] = history_dicts
|
| 204 |
+
save_storage(storage)
|
| 205 |
+
return "", history_dicts
|
| 206 |
+
|
| 207 |
+
# Use persisted storage as source of truth — not Gradio input — so full history is always kept
|
| 208 |
+
history_dicts = list(storage.get("scraper_history", []))
|
| 209 |
+
history_dicts.append({"role": "user", "content": message.strip()})
|
| 210 |
+
|
| 211 |
+
system_prompt = _build_system_prompt("scraper")
|
| 212 |
+
messages = [{"role": "system", "content": system_prompt}]
|
| 213 |
+
for m in history_dicts:
|
| 214 |
+
if m.get("role") in ("user", "assistant"):
|
| 215 |
+
messages.append({"role": m["role"], "content": m.get("content", "")})
|
| 216 |
+
|
| 217 |
+
try:
|
| 218 |
+
resp = client.chat.completions.create(
|
| 219 |
+
model="llama-3.1-8b-instant",
|
| 220 |
+
messages=messages,
|
| 221 |
+
max_tokens=1024,
|
| 222 |
+
temperature=0.3,
|
| 223 |
+
)
|
| 224 |
+
reply = resp.choices[0].message.content
|
| 225 |
+
except Exception as e:
|
| 226 |
+
reply = f"Error communicating with Groq: {e}"
|
| 227 |
+
|
| 228 |
+
history_dicts.append({"role": "assistant", "content": reply})
|
| 229 |
+
storage["scraper_history"] = history_dicts
|
| 230 |
+
save_storage(storage)
|
| 231 |
+
|
| 232 |
+
return "", history_dicts
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
def chat_turn_youtube(message: str, _history_ignored):
|
| 236 |
+
"""One turn in YouTube tab: use storage as source of truth, same pattern as scraper."""
|
| 237 |
+
if not message or not message.strip():
|
| 238 |
+
return "", storage.get("youtube_history", [])
|
| 239 |
+
|
| 240 |
+
context = contexts.get("youtube", "") or ""
|
| 241 |
+
if not context.strip():
|
| 242 |
+
history_dicts = list(storage.get("youtube_history", []))
|
| 243 |
+
history_dicts.append({"role": "user", "content": message.strip()})
|
| 244 |
+
history_dicts.append(
|
| 245 |
+
{
|
| 246 |
+
"role": "assistant",
|
| 247 |
+
"content": "Please fetch a YouTube transcript first, then ask questions.",
|
| 248 |
+
}
|
| 249 |
+
)
|
| 250 |
+
storage["youtube_history"] = history_dicts
|
| 251 |
+
save_storage(storage)
|
| 252 |
+
return "", history_dicts
|
| 253 |
+
|
| 254 |
+
# Use persisted storage as source of truth
|
| 255 |
+
history_dicts = list(storage.get("youtube_history", []))
|
| 256 |
+
history_dicts.append({"role": "user", "content": message.strip()})
|
| 257 |
+
|
| 258 |
+
system_prompt = _build_system_prompt("youtube")
|
| 259 |
+
messages = [{"role": "system", "content": system_prompt}]
|
| 260 |
+
for m in history_dicts:
|
| 261 |
+
if m.get("role") in ("user", "assistant"):
|
| 262 |
+
messages.append({"role": m["role"], "content": m.get("content", "")})
|
| 263 |
+
|
| 264 |
+
try:
|
| 265 |
+
resp = client.chat.completions.create(
|
| 266 |
+
model="llama-3.1-8b-instant",
|
| 267 |
+
messages=messages,
|
| 268 |
+
max_tokens=1024,
|
| 269 |
+
temperature=0.3,
|
| 270 |
+
)
|
| 271 |
+
reply = resp.choices[0].message.content
|
| 272 |
+
except Exception as e:
|
| 273 |
+
reply = f"Error communicating with Groq: {e}"
|
| 274 |
+
|
| 275 |
+
history_dicts.append({"role": "assistant", "content": reply})
|
| 276 |
+
storage["youtube_history"] = history_dicts
|
| 277 |
+
save_storage(storage)
|
| 278 |
+
|
| 279 |
+
return "", history_dicts
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
def save_preferences(prefs: str):
|
| 283 |
+
"""Save user preferences to storage and confirm."""
|
| 284 |
+
global storage
|
| 285 |
+
storage["user_preferences"] = prefs or ""
|
| 286 |
+
save_storage(storage)
|
| 287 |
+
return "Preferences saved. They will be applied to all future replies."
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
# ---------------------------------------------------------------------------
|
| 291 |
+
# Gradio UI
|
| 292 |
+
# ---------------------------------------------------------------------------
|
| 293 |
+
|
| 294 |
+
# Start with empty chat; history appends as the user sends messages (and is still saved to storage)
|
| 295 |
+
with gr.Blocks(title="Scraper Bot v3 — Multi-Turn Chat with Memory") as demo:
|
| 296 |
+
gr.Markdown("# Version 3 — Multi-Turn AI Chatbot with Persistent Storage")
|
| 297 |
+
gr.Markdown(
|
| 298 |
+
"Chat with memory: conversation history is kept and sent to the AI so you can ask follow-ups. "
|
| 299 |
+
"History and preferences are saved to `chat_storage.json` and persist across restarts."
|
| 300 |
+
)
|
| 301 |
+
|
| 302 |
+
with gr.Row():
|
| 303 |
+
with gr.Column(scale=1):
|
| 304 |
+
gr.Markdown("### User preferences (editable)")
|
| 305 |
+
gr.Markdown(
|
| 306 |
+
"These are injected into the system prompt on every reply. Examples: "
|
| 307 |
+
"*Always respond formally*, *Cite sources when summarizing*, *Keep answers under 3 sentences*."
|
| 308 |
+
)
|
| 309 |
+
prefs_input = gr.Textbox(
|
| 310 |
+
label="Preferences",
|
| 311 |
+
value=storage.get("user_preferences", ""),
|
| 312 |
+
placeholder="e.g., Always respond formally. Cite sources when summarizing.",
|
| 313 |
+
lines=3,
|
| 314 |
+
)
|
| 315 |
+
save_prefs_btn = gr.Button("Save preferences")
|
| 316 |
+
prefs_status = gr.Textbox(label="Status", interactive=False)
|
| 317 |
+
save_prefs_btn.click(
|
| 318 |
+
save_preferences,
|
| 319 |
+
inputs=[prefs_input],
|
| 320 |
+
outputs=[prefs_status],
|
| 321 |
+
)
|
| 322 |
+
|
| 323 |
+
with gr.Column(scale=2):
|
| 324 |
+
with gr.Tabs():
|
| 325 |
+
with gr.TabItem("Bot-Protected Website Scraper"):
|
| 326 |
+
gr.Markdown(
|
| 327 |
+
"Enter a URL to scrape (e.g. Goodreads). Then chat; history is kept for follow-up questions."
|
| 328 |
+
)
|
| 329 |
+
with gr.Row():
|
| 330 |
+
url_input = gr.Textbox(
|
| 331 |
+
label="URL",
|
| 332 |
+
placeholder="https://www.goodreads.com/",
|
| 333 |
+
scale=3,
|
| 334 |
+
)
|
| 335 |
+
scrape_btn = gr.Button("Scrape URL", scale=1)
|
| 336 |
+
scrape_status = gr.Textbox(label="Status", interactive=False)
|
| 337 |
+
scrape_preview = gr.Textbox(
|
| 338 |
+
label="Content preview", interactive=False, lines=4
|
| 339 |
+
)
|
| 340 |
+
scraper_chatbot = gr.Chatbot(
|
| 341 |
+
value=storage.get("scraper_history", []),
|
| 342 |
+
height=400,
|
| 343 |
+
label="Chat (multi-turn, persisted)",
|
| 344 |
+
)
|
| 345 |
+
scraper_msg = gr.Textbox(
|
| 346 |
+
label="Message",
|
| 347 |
+
placeholder="e.g., What are the top 5 books? Then: Tell me more about the second one.",
|
| 348 |
+
)
|
| 349 |
+
scrape_btn.click(
|
| 350 |
+
scrape_website,
|
| 351 |
+
inputs=[url_input],
|
| 352 |
+
outputs=[scrape_status, scrape_preview],
|
| 353 |
+
)
|
| 354 |
+
scraper_msg.submit(
|
| 355 |
+
chat_turn_scraper,
|
| 356 |
+
inputs=[scraper_msg, scraper_chatbot],
|
| 357 |
+
outputs=[scraper_msg, scraper_chatbot],
|
| 358 |
+
)
|
| 359 |
+
|
| 360 |
+
with gr.TabItem("YouTube Transcript Q&A"):
|
| 361 |
+
gr.Markdown(
|
| 362 |
+
"Enter a YouTube video URL or ID to fetch its transcript, then chat with multi-turn memory."
|
| 363 |
+
)
|
| 364 |
+
with gr.Row():
|
| 365 |
+
yt_input = gr.Textbox(
|
| 366 |
+
label="YouTube URL or ID",
|
| 367 |
+
placeholder="dQw4w9WgXcQ",
|
| 368 |
+
scale=3,
|
| 369 |
+
)
|
| 370 |
+
yt_btn = gr.Button("Get Transcript", scale=1)
|
| 371 |
+
yt_status = gr.Textbox(label="Status", interactive=False)
|
| 372 |
+
yt_preview = gr.Textbox(
|
| 373 |
+
label="Transcript preview", interactive=False, lines=4
|
| 374 |
+
)
|
| 375 |
+
yt_chatbot = gr.Chatbot(
|
| 376 |
+
value=storage.get("youtube_history", []),
|
| 377 |
+
height=400,
|
| 378 |
+
label="Chat (multi-turn, persisted)",
|
| 379 |
+
)
|
| 380 |
+
yt_msg = gr.Textbox(
|
| 381 |
+
label="Message",
|
| 382 |
+
placeholder="e.g., Summarize the video. Then: What are the main takeaways?",
|
| 383 |
+
)
|
| 384 |
+
yt_btn.click(
|
| 385 |
+
fetch_youtube_transcript,
|
| 386 |
+
inputs=[yt_input],
|
| 387 |
+
outputs=[yt_status, yt_preview],
|
| 388 |
+
)
|
| 389 |
+
yt_msg.submit(
|
| 390 |
+
chat_turn_youtube,
|
| 391 |
+
inputs=[yt_msg, yt_chatbot],
|
| 392 |
+
outputs=[yt_msg, yt_chatbot],
|
| 393 |
+
)
|
| 394 |
+
|
| 395 |
+
if __name__ == "__main__":
|
| 396 |
+
if os.environ.get("SPACE_ID"):
|
| 397 |
+
demo.launch() # Hugging Face Spaces: use their host/port
|
| 398 |
+
else:
|
| 399 |
+
demo.launch(server_name="127.0.0.1", server_port=7863) # Local
|
requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio>=4.0.0,<7.0.0
|
| 2 |
+
groq>=0.4.0
|
| 3 |
+
beautifulsoup4>=4.12.0
|
| 4 |
+
requests>=2.28.0
|
| 5 |
+
python-dotenv>=1.0.0
|
| 6 |
+
youtube-transcript-api>=1.0.0
|
storage.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Persistent storage for Version 3: conversation history and user preferences.
|
| 3 |
+
Uses a local JSON file (chat_storage.json) for cross-session persistence.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import json
|
| 7 |
+
import os
|
| 8 |
+
|
| 9 |
+
STORAGE_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "chat_storage.json")
|
| 10 |
+
|
| 11 |
+
DEFAULT_STORAGE = {
|
| 12 |
+
"scraper_history": [],
|
| 13 |
+
"youtube_history": [],
|
| 14 |
+
"user_preferences": "",
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def load_storage() -> dict:
|
| 19 |
+
"""Load conversation history and user preferences from JSON. Creates file if missing."""
|
| 20 |
+
if not os.path.exists(STORAGE_PATH):
|
| 21 |
+
return dict(DEFAULT_STORAGE)
|
| 22 |
+
try:
|
| 23 |
+
with open(STORAGE_PATH, "r", encoding="utf-8") as f:
|
| 24 |
+
data = json.load(f)
|
| 25 |
+
data.setdefault("scraper_history", [])
|
| 26 |
+
data.setdefault("youtube_history", [])
|
| 27 |
+
data.setdefault("user_preferences", "")
|
| 28 |
+
return data
|
| 29 |
+
except (json.JSONDecodeError, IOError):
|
| 30 |
+
return dict(DEFAULT_STORAGE)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def save_storage(data: dict) -> None:
|
| 34 |
+
"""Write storage to JSON."""
|
| 35 |
+
with open(STORAGE_PATH, "w", encoding="utf-8") as f:
|
| 36 |
+
json.dump(data, f, indent=2, ensure_ascii=False)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def history_dicts_to_tuples(history: list) -> list:
|
| 40 |
+
"""Convert [{"role":"user","content":...},{"role":"assistant","content":...}, ...] to [(u,a), ...] for Gradio Chatbot."""
|
| 41 |
+
tuples = []
|
| 42 |
+
i = 0
|
| 43 |
+
while i < len(history):
|
| 44 |
+
msg = history[i]
|
| 45 |
+
if not isinstance(msg, dict):
|
| 46 |
+
i += 1
|
| 47 |
+
continue
|
| 48 |
+
role = msg.get("role")
|
| 49 |
+
content = msg.get("content") or ""
|
| 50 |
+
if role == "user":
|
| 51 |
+
next_msg = history[i + 1] if i + 1 < len(history) else None
|
| 52 |
+
if next_msg and isinstance(next_msg, dict) and next_msg.get("role") == "assistant":
|
| 53 |
+
tuples.append((content, next_msg.get("content") or ""))
|
| 54 |
+
i += 2
|
| 55 |
+
else:
|
| 56 |
+
tuples.append((content, ""))
|
| 57 |
+
i += 1
|
| 58 |
+
else:
|
| 59 |
+
i += 1
|
| 60 |
+
return tuples
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def history_tuples_to_dicts(tuples: list) -> list:
|
| 64 |
+
"""Convert Gradio Chatbot [(user, assistant), ...] to [{"role":"user",...},{"role":"assistant",...}, ...]."""
|
| 65 |
+
out = []
|
| 66 |
+
for user_msg, assistant_msg in tuples or []:
|
| 67 |
+
if user_msg:
|
| 68 |
+
out.append({"role": "user", "content": str(user_msg)})
|
| 69 |
+
if assistant_msg:
|
| 70 |
+
out.append({"role": "assistant", "content": str(assistant_msg)})
|
| 71 |
+
return out
|