File size: 21,195 Bytes
a71ea0a 080bb85 826090a a71ea0a 66c4741 a71ea0a 2f50a55 a71ea0a 2f50a55 a71ea0a 2f50a55 a71ea0a 2f50a55 a71ea0a 2f50a55 a71ea0a efd13db a71ea0a 62ef269 a71ea0a efd13db a71ea0a 2f50a55 a71ea0a 2f50a55 a71ea0a 99c1869 dce9c5d 99c1869 dce9c5d 99c1869 43951a2 a71ea0a 43951a2 66c4741 b2be33e a71ea0a 66c4741 a71ea0a 43951a2 99c1869 43951a2 99c1869 66c4741 49fdc18 66c4741 49fdc18 66c4741 49fdc18 66c4741 99c1869 a71ea0a 99c1869 66c4741 49fdc18 66c4741 a71ea0a 080bb85 99c1869 dce9c5d 99c1869 080bb85 66c4741 a71ea0a 66c4741 a71ea0a 66c4741 a71ea0a fbbc5a8 66c4741 a71ea0a 66c4741 fbbc5a8 66c4741 ef690b5 e5ce840 ef690b5 0f1de32 ef690b5 0f1de32 66c4741 a71ea0a 9a1fe75 7e002df 9a1fe75 7e002df 9a1fe75 588cdee 7e002df 080bb85 66c4741 080bb85 ee5f55f 54f1b53 f4e4cf0 e5ce840 cf1d4c5 54f1b53 cf1d4c5 54f1b53 cf1d4c5 54f1b53 cf1d4c5 54f1b53 080bb85 66c4741 49fdc18 43951a2 eb02516 43951a2 66c4741 fbbc5a8 66c4741 a71ea0a 66c4741 080bb85 66c4741 a71ea0a 9977437 a71ea0a 9977437 a71ea0a 9977437 a71ea0a 2b094c5 a5e7a1a a71ea0a a5e7a1a a71ea0a d1d92a8 a71ea0a 91ed532 de5b225 91ed532 a71ea0a 9977437 a71ea0a 9977437 a71ea0a a5e7a1a a71ea0a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 | """
Gradio app for Hugging Face chatbot with RAG capabilities.
"""
import warnings
# Suppress deprecation from dependencies (e.g. accelerate) until they use torch.distributed.ReduceOp
warnings.filterwarnings(
"ignore",
message=".*torch.distributed.reduce_op.*ReduceOp.*",
category=FutureWarning,
)
import re
import gradio as gr
from gradio.themes.base import Base
from gradio.themes.utils import colors, fonts, sizes
import os
from typing import List, Tuple
from huggingface_hub import InferenceClient
from ingestion import DocumentIngestion
# Create a clean minimalist theme
class MinimalistTheme(Base):
"""A clean, minimalist theme with subtle colors and simple styling."""
def __init__(self):
super().__init__(
primary_hue=colors.blue,
secondary_hue=colors.gray,
neutral_hue=colors.gray,
spacing_size=sizes.spacing_md,
radius_size=sizes.radius_sm,
text_size=sizes.text_md,
font=(
fonts.GoogleFont("Inter"),
"ui-sans-serif",
"system-ui",
"sans-serif",
),
font_mono=(
fonts.GoogleFont("JetBrains Mono"),
"ui-monospace",
"monospace",
),
)
super().set(
# Clean backgrounds
background_fill_primary="#ffffff",
background_fill_primary_dark="#1a1a1a",
background_fill_secondary="#f3f6ee",
background_fill_secondary_dark="#24372b",
body_background_fill="#f3f6ee",
body_background_fill_dark="#ffffff",
block_background_fill="#f3f6ee",
block_background_fill_dark="#24372b",
# Subtle borders
block_border_width="1px",
block_border_color="#e0e0e0",
block_border_color_dark="#2a2a2a",
block_shadow="none",
# Clean buttons
button_primary_background_fill="#8abf50",
button_primary_background_fill_hover="#1d4ed8",
button_primary_text_color="#ffffff",
button_primary_background_fill_dark="#8abf50",
button_primary_background_fill_hover_dark="#2563eb",
button_secondary_background_fill="#247b55",
button_secondary_background_fill_hover="#e5e7eb",
button_secondary_text_color="#ffffff",
button_secondary_background_fill_dark="#374151",
button_secondary_background_fill_hover_dark="#4b5563",
button_border_width="0px",
# Input fields
input_background_fill="#ffffff",
input_background_fill_dark="#ffffff",
input_border_width="1px",
input_border_color="#d1d5db",
input_border_color_dark="#374151",
# Text colors
body_text_color="#1e1e1e",
body_text_color_dark="#e5e7eb",
block_label_text_color="#1e1e1e",
block_label_text_color_dark="#9ca3af",
)
class RAGChatbot:
"""Chatbot with RAG capabilities."""
# Default and fallback models (try in order until one is supported by your Inference API providers)
DEFAULT_CHAT_MODEL = "HuggingFaceH4/zephyr-7b-beta"
FALLBACK_CHAT_MODELS = [
"mistralai/Mixtral-8x7B-Instruct-v0.1",
"meta-llama/Llama-3.2-3B-Instruct",
"Qwen/Qwen2.5-7B-Instruct",
]
def __init__(
self,
model_name: str = None,
embedding_model: str = "all-mpnet-base-v2",
vector_store_path: str = "data/vector_store"
):
"""
Initialize the RAG chatbot.
Args:
model_name: Hugging Face model name for the chatbot (via Inference API)
embedding_model: Model for document embeddings
vector_store_path: Path to saved vector store
"""
self.model_name = model_name if model_name else self.DEFAULT_CHAT_MODEL
# Build list of models to try (primary first, then fallbacks not already primary)
self._models_to_try = [self.model_name] + [
m for m in self.FALLBACK_CHAT_MODELS if m != self.model_name
]
# Initialize Inference API client (no model in constructor so we can try multiple)
hf_token = os.environ.get("HF_TOKEN")
# Debug: report HF_TOKEN status (masked)
if not hf_token:
print("[DEBUG] HF_TOKEN: not set (empty or missing)")
print("Warning: HF_TOKEN not set. Inference API calls may fail.")
print("Set HF_TOKEN environment variable or add it to Space secrets.")
else:
masked = f"{hf_token[:4]}...{hf_token[-4:]}" if len(hf_token) > 8 else "****"
print(f"[DEBUG] HF_TOKEN: set (length={len(hf_token)}, masked={masked})")
print("HF_TOKEN found. Inference API ready.")
print(f"[DEBUG] Inference API client (models to try: {self._models_to_try})")
try:
self.inference_client = InferenceClient(token=hf_token)
print("[DEBUG] Inference API client initialized (model chosen per request with fallbacks)")
except Exception as e:
print(f"[DEBUG] Error initializing Inference API client: {type(e).__name__}: {e}")
self.inference_client = None
# Initialize document ingestion
self.ingestion = DocumentIngestion(embedding_model=embedding_model)
# Load vector store if it exists
if os.path.exists(vector_store_path) and os.path.exists(
os.path.join(vector_store_path, "index.faiss")
):
try:
self.ingestion.load(vector_store_path)
print("Loaded existing vector store")
except Exception as e:
print(f"Could not load vector store: {e}")
self.chat_history = []
def _generate_with_chat(self, user_content: str, max_new_tokens: int = 512) -> str:
"""Call the Inference API using chat_completion; try fallback models if current is not supported."""
last_error = None
for model in self._models_to_try:
print(f"[DEBUG] _generate_with_chat: trying model={model}, prompt_len={len(user_content)}, max_tokens={max_new_tokens}")
try:
response = self.inference_client.chat_completion(
model=model,
messages=[{"role": "user", "content": user_content}],
max_tokens=max_new_tokens,
temperature=0.7,
)
print(f"[DEBUG] chat_completion OK for model={model}, response type: {type(response).__name__}")
if response and response.choices and len(response.choices) > 0:
msg = response.choices[0].message
if hasattr(msg, "content") and msg.content:
# Remember this model for next time
self.model_name = model
self._models_to_try = [model] + [m for m in self._models_to_try if m != model]
return msg.content.strip()
print("[DEBUG] chat_completion returned empty or unexpected structure")
except Exception as e:
last_error = e
err_str = str(e).lower()
if (
"model_not_supported" in err_str
or "not supported by any provider" in err_str
or "410" in err_str
or "gone" in err_str
):
print(f"[DEBUG] Model {model} not available, trying next fallback.")
continue
print(f"[DEBUG] _generate_with_chat exception for {model}: {type(e).__name__}: {e}")
import traceback
traceback.print_exc()
raise
if last_error is not None:
raise last_error
return ""
def generate_response(self, query: str, use_rag: bool = True, num_results: int = 5) -> str:
"""
Generate a response to the user query using RAG and Inference API.
Args:
query: User's question
use_rag: Whether to use RAG (retrieve relevant documents)
num_results: Number of document chunks to retrieve
Returns:
Generated response
"""
if self.inference_client is None:
return "Error: Inference API client not initialized. Please check HF_TOKEN configuration."
# If RAG is enabled and we have a vector store, retrieve context and generate answer
if use_rag and self.ingestion.index is not None:
try:
results = self.ingestion.search(query, k=num_results)
if results:
# Build context from retrieved chunks; include source/title so the model can cite it
context_parts = []
for i, result in enumerate(results, 1):
text = result['text'].strip()
if not text:
continue
meta = result.get('metadata') or {}
source_label = meta.get('document_title') or meta.get('source') or f"Source {i}"
context_parts.append(f"[Context {i}] (Source: {source_label})\n{text}")
context = "\n\n".join(context_parts)
# Build instruction-tuned prompt
prompt = f"""
*You are an expert assistant specializing in organic farming, in particular in Canada and its legal context.
Answer the user's question using only the information provided in the context.
If the context does not include the information needed to answer the question, clearly say:
"The provided context does not contain enough information to answer this question."
Do not alter or paraphrase this exact phrase.
When answering:
Respond in English only.
Do not use outside knowledge, assumptions, or guesswork.
Do not reference or name the source documents anywhere in your answer.
Provide concise, accurate, and helpful explanations.
Do not reveal your internal reasoning. Provide only the final answer.
Structure your answer in the following format:
Summary — A brief, high‑level answer.
Supporting Details — Explain using information only from the provided context. Do not cite or name sources inline.
Context:
{context}
Question: {query}
Answer:"""
# Build mapping from context index to source label (and URL if applicable)
context_index_to_source = {}
context_index_to_url = {}
for i, result in enumerate(results, 1):
meta = result.get("metadata") or {}
context_index_to_source[i] = (
meta.get("document_title") or meta.get("source") or f"Source {i}"
)
if meta.get("type") == "url" and meta.get("source"):
context_index_to_url[i] = meta["source"]
elif meta.get("url"):
context_index_to_url[i] = meta["url"]
# Generate response using chat/comversational API (Mistral instruct uses this)
try:
response_text = self._generate_with_chat(prompt, max_new_tokens=512)
if response_text:
# Strip all inline [Context N] references and bare "Context N" mentions from the body
response_text = re.sub(r'\[Context\s+\d+\]', '', response_text)
response_text = re.sub(r'(?<!\[)\bContext\s+[\d][,\s\d]*', '', response_text)
response_text = re.sub(r'\s{2,}', ' ', response_text)
# Only append references when the model actually answered
no_answer_phrase = "The provided context does not contain enough information to answer this question."
no_answer_replacement = "The organic documents provided do not contain enough information to answer your question. For more information, try taking a look at the [Canadian Food Inspection Agency](https://inspection.canada.ca/en/food-labels/organic-products/), the [Certified Organic Growers of Canada](https://cog.ca/), or the [Canada Organic Trade Association](https://canada-organic.ca/en/organic)."
if no_answer_phrase in response_text:
response_text = response_text.replace(no_answer_phrase, no_answer_replacement)
if no_answer_replacement not in response_text:
# Deduplicate: one entry per unique source URL (or label)
seen = {}
for i, source_label in context_index_to_source.items():
url = context_index_to_url.get(i)
key = url or source_label
if key not in seen:
seen[key] = (source_label, url)
ref_lines = [
"",
"---",
"**References**",
]
for idx, (key, (source_label, url)) in enumerate(seen.items(), 1):
if url:
ref_lines.append(f"{idx}. [{source_label}]({url})")
else:
ref_lines.append(f"{idx}. {source_label}")
response_text = response_text.rstrip() + "\n\n" + "\n".join(ref_lines)
return response_text
raise ValueError("Empty response from model")
except Exception as api_error:
print(f"[DEBUG] RAG generation failed: {type(api_error).__name__}: {api_error}")
err_str = str(api_error).lower()
if "model_not_supported" in err_str or "not supported by any provider" in err_str:
return (
"None of the configured chat models are available with your Inference API providers.\n\n"
"**How to fix:**\n"
"1. See which models are available: https://huggingface.co/inference/models\n"
"2. Enable providers (and pick a chat model): https://huggingface.co/settings/inference-api\n"
"3. In app.py, set RAGChatbot(model_name=\"your-chosen-model-id\") to match a model you enabled."
)
# Fallback: return formatted chunks with note
response_parts = []
response_parts.append("I retrieved relevant information, but couldn't generate a synthesized answer. Here are the relevant chunks:\n\n")
for i, result in enumerate(results, 1):
meta = result.get('metadata') or {}
source = meta.get('document_title') or meta.get('source', '')
text = result['text'].strip()
if text:
response_parts.append(f"**Relevant information {i}** (from {source}):\n{text}\n")
return "\n".join(response_parts)
else:
# No results found
return "I couldn't find any relevant information in the documents to answer your question. Please try rephrasing or check if the documents contain information about this topic."
except Exception as e:
print(f"Error in RAG retrieval: {e}")
return f"I encountered an error while searching the documents: {str(e)}"
# If no RAG or no vector store, generate response without context
try:
prompt = f"""You are a helpful assistant. Answer the following question concisely.
Question: {query}
Answer:"""
response_text = self._generate_with_chat(prompt, max_new_tokens=256)
if response_text:
return response_text
return "I couldn't generate a response. Please try again."
except Exception as e:
print(f"Error generating response: {e}")
return f"I encountered an error while generating a response: {str(e)}. Please check your HF_TOKEN configuration."
def chat(self, message: str, history):
"""
Handle chat interaction.
Args:
message: User message
history: Chat history (list of ChatMessage or dicts with 'role' and 'content')
Returns:
Updated history
"""
if not message or not message.strip():
return "", history or []
# Ensure history is a list
if history is None:
history = []
# Add user message as dictionary
history.append({"role": "user", "content": message})
# Generate response (always use RAG)
try:
response = self.generate_response(message, use_rag=True)
# Ensure response is not empty
if not response or not response.strip():
response = "I'm sorry, I couldn't generate a response. Please try again."
except Exception as e:
print(f"Error generating response: {e}")
import traceback
traceback.print_exc()
response = f"I encountered an error: {str(e)}"
# Add assistant response as dictionary
history.append({"role": "assistant", "content": response})
print(f"Debug - History length: {len(history)}")
print(f"Debug - Response: {response[:100] if response else 'None'}...")
return "", history
# Initialize chatbot
chatbot = RAGChatbot()
# Create Gradio interface
custom_css = """
.gradio-container textarea {
color: #1e1e1e !important;
}
"""
with gr.Blocks(title="Organic Certification Assistant", css=custom_css) as app:
gr.Markdown("<h1 style='color: #24372b; font-size: 2.5rem;'>👤 Organic Certification Assistant</h1>")
chatbot_interface = gr.Chatbot(
label="Chat",
height=500,
value=[{"role": "assistant", "content": "Welcome to the Organic Certification Assistant! Ask me any questions you have about organic certification and operation in Canada."}]
)
with gr.Row():
msg = gr.Textbox(
label="Your Message",
placeholder="Ask a question about Canadian organics...",
scale=4
)
with gr.Row():
submit_btn = gr.Button("Send", variant="primary")
clear_btn = gr.Button("Clear")
with gr.Accordion("Example questions", open=False):
gr.Markdown("""
- What are the general principles of organic production in Canada?
- What substances are permitted for use in organic crop production?
- Can I use synthetic pesticides on an organic farm?
- What are the requirements for transitioning land to organic certification?
- What livestock practices are required under Canadian organic standards?
- Are antibiotics allowed in organic livestock production?
- What labelling requirements apply to organic products in Canada?
- What is the difference between "organic" and "made with organic ingredients" on a label?
- What are the permitted substances for organic aquaculture in Canada?
- Who certifies organic products in Canada?
> **Disclaimer:** AI-generated responses may not always be accurate or complete. Always verify the information provided against the original source documents and consult official resources before making decisions.
""")
msg.submit(
chatbot.chat,
inputs=[msg, chatbot_interface],
outputs=[msg, chatbot_interface]
)
submit_btn.click(
chatbot.chat,
inputs=[msg, chatbot_interface],
outputs=[msg, chatbot_interface]
)
def clear_chat():
return [{"role": "assistant", "content": "Welcome to the Organic Certification Assistant! Ask me any questions you have about organic certification and operation in Canada."}], ""
clear_btn.click(clear_chat, outputs=[chatbot_interface, msg])
if __name__ == "__main__":
# Get port from environment variable (Hugging Face Spaces sets this) or default to 7860
port = int(os.environ.get("PORT", 7860))
app.launch(
share=False,
server_name="0.0.0.0",
server_port=port,
theme=MinimalistTheme()
)
|