far-chatbot / app.py
collinschreyer-dev
Fix sample question buttons to trigger rerun
02134bc
Raw
History Blame Contribute Delete
7.86 kB
#!/usr/bin/env python3
"""
FAR Chatbot - Hugging Face Spaces Version
Federal Acquisition Regulation Assistant with Clickable Citations
"""
import streamlit as st
import time
import logging
import os
import sys
import re
from datetime import datetime
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# Import the chatbot
from far_chatbot import FARChatbot
try:
import markdown
except ImportError:
markdown = None
# Configure page
st.set_page_config(
page_title="FAR Chatbot - Federal Acquisition Regulation Assistant",
page_icon="πŸ›οΈ",
layout="wide",
initial_sidebar_state="expanded"
)
# Authentication
VALID_CREDENTIALS = {"testuser": "farbot2025"}
def check_password():
def password_entered():
username = st.session_state.get("username", "")
password = st.session_state.get("password", "")
if username in VALID_CREDENTIALS and VALID_CREDENTIALS[username] == password:
st.session_state["authenticated"] = True
del st.session_state["password"]
else:
st.session_state["authenticated"] = False
if "authenticated" not in st.session_state:
st.session_state["authenticated"] = False
if not st.session_state["authenticated"]:
col1, col2, col3 = st.columns([1, 2, 1])
with col2:
st.markdown("## πŸ›οΈ FAR Chatbot Login")
st.text_input("Username", key="username")
st.text_input("Password", type="password", key="password")
st.button("πŸ” Log In", on_click=password_entered, type="primary", use_container_width=True)
if st.session_state.get("authenticated") == False and "username" in st.session_state and st.session_state["username"]:
st.error("❌ Invalid credentials")
return False
return True
if not check_password():
st.stop()
# CSS Styling
st.markdown("""
<style>
.main-header {
text-align: center; padding: 1.5rem;
background: linear-gradient(135deg, #1a365d 0%, #2c5282 50%, #2b6cb0 100%);
color: white; border-radius: 16px; margin-bottom: 2rem;
}
.main-header h1 { margin: 0; font-size: 2.5rem; }
.main-header p { margin: 0.5rem 0 0 0; opacity: 0.9; }
.user-message {
background: #f7fafc; padding: 1rem; border-radius: 16px;
margin: 1rem 0; border-left: 4px solid #e53e3e;
}
.bot-message {
background: linear-gradient(135deg, #ebf8ff 0%, #e6fffa 100%);
padding: 1.25rem; border-radius: 16px; margin: 1rem 0;
border-left: 4px solid #2b6cb0;
}
.citation-link {
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
padding: 2px 8px; border-radius: 6px; font-family: monospace;
font-weight: 600; color: #92400e; text-decoration: none;
border: 1px solid #f59e0b;
}
.citation-link:hover { background: #fde68a; }
.source-card {
background: white; border: 1px solid #e2e8f0;
border-radius: 12px; padding: 1rem; margin: 0.75rem 0;
}
</style>
""", unsafe_allow_html=True)
# Session state
if 'chatbot' not in st.session_state:
st.session_state.chatbot = None
if 'chat_history' not in st.session_state:
st.session_state.chat_history = []
if 'show_sources' not in st.session_state:
st.session_state.show_sources = True
@st.cache_resource
def load_chatbot():
"""Load the FAR chatbot"""
try:
logger.info("Loading FAR Chatbot...")
chatbot = FARChatbot(
faiss_index_path="data/faiss_index.index",
texts_path="data/texts.txt",
use_gpt5=True
)
return chatbot
except Exception as e:
logger.error(f"Error loading chatbot: {e}")
st.error(f"Error loading chatbot: {e}")
return None
def get_acquisition_gov_url(citation: str) -> str:
base_citation = re.sub(r'\([a-z]\)$', '', citation)
return f"https://www.acquisition.gov/far/{base_citation}"
def make_citations_clickable(response: str, search_results: list) -> str:
if not markdown:
return response
text = response
def make_link(citation):
url = get_acquisition_gov_url(citation)
return f'<a href="{url}" target="_blank" class="citation-link">'
# Replace citation patterns
text = re.sub(r'\[FAR\s+(\d+\.\d+(?:-\d+)?(?:\([a-z]\))?)\]',
lambda m: f'{make_link(m.group(1))}[{m.group(1)}]</a>', text)
text = re.sub(r'\[(\d+\.\d+(?:-\d+)?(?:\([a-z]\))?)\]',
lambda m: f'{make_link(m.group(1))}[{m.group(1)}]</a>', text)
text = re.sub(r'(?<!View )FAR\s+(\d+\.\d+(?:-\d+)?(?:\([a-z]\))?)',
lambda m: f'{make_link(m.group(1))}FAR {m.group(1)}</a>', text)
return markdown.markdown(text, extensions=['tables', 'fenced_code', 'nl2br'])
# Header
st.markdown("""
<div class="main-header">
<h1>πŸ›οΈ FAR Chatbot</h1>
<p>Federal Acquisition Regulation Assistant</p>
</div>
""", unsafe_allow_html=True)
# Sidebar
with st.sidebar:
st.markdown("## βš™οΈ Settings")
if st.session_state.chatbot is None:
st.session_state.chatbot = load_chatbot()
if st.session_state.chatbot:
st.success("βœ… Chatbot Ready!")
else:
st.error("❌ Failed to load")
st.session_state.show_sources = st.checkbox("Show sources", value=True)
st.markdown("### πŸ’‘ Sample Questions")
samples = [
"What are small business set-asides?",
"Explain the simplified acquisition threshold",
"What is the micro-purchase threshold?",
"When can I use sole source?"
]
for q in samples:
if st.button(f"πŸ’¬ {q}", key=f"s_{hash(q)}"):
st.session_state.current_question = q
st.rerun()
if st.button("πŸ—‘οΈ Clear Chat"):
st.session_state.chat_history = []
st.rerun()
if st.button("πŸšͺ Logout"):
st.session_state["authenticated"] = False
st.rerun()
# Main content
if st.session_state.chatbot is None:
st.error("Chatbot not loaded")
st.stop()
# Display chat history
for entry in st.session_state.chat_history:
question, answer, search_results, timestamp = entry[:4]
st.markdown(f'<div class="user-message">πŸ‘€ <b>You:</b> {question}</div>', unsafe_allow_html=True)
formatted = make_citations_clickable(answer, search_results)
st.markdown(f'<div class="bot-message">πŸ€– <b>FAR Bot:</b><br>{formatted}</div>', unsafe_allow_html=True)
if st.session_state.show_sources and search_results:
with st.expander("πŸ“š Sources"):
for cit, txt in search_results[:5]:
url = get_acquisition_gov_url(cit)
st.markdown(f"**[FAR {cit}]({url})**: {txt[:300]}...")
# Input
st.markdown("## πŸ’¬ Ask a Question")
question = ""
if 'current_question' in st.session_state:
question = st.session_state.current_question
del st.session_state.current_question
question = st.text_input("Your question:", value=question, placeholder="Ask about FAR regulations...")
if st.button("πŸš€ Ask", type="primary") and question.strip():
with st.spinner("Processing..."):
try:
result = st.session_state.chatbot.chat(question, top_k=None)
timestamp = datetime.now().strftime("%H:%M")
st.session_state.chat_history.append((
question,
result['response'],
result.get('search_results', []),
timestamp
))
st.rerun()
except Exception as e:
st.error(f"Error: {e}")
st.markdown("---")
st.markdown("πŸ›οΈ FAR Chatbot | Powered by GPT-4 Turbo | [acquisition.gov](https://acquisition.gov)")