collinschreyer-dev commited on
Commit
ea523d0
·
0 Parent(s):

Initial FAR Chatbot deployment with LFS

Browse files
Files changed (8) hide show
  1. .gitattributes +1 -0
  2. Dockerfile +25 -0
  3. README.md +35 -0
  4. app.py +235 -0
  5. data/faiss_index.index +3 -0
  6. data/texts.txt +0 -0
  7. far_chatbot.py +164 -0
  8. requirements.txt +6 -0
.gitattributes ADDED
@@ -0,0 +1 @@
 
 
1
+ *.index filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install system dependencies
6
+ RUN apt-get update && apt-get install -y \
7
+ build-essential \
8
+ && rm -rf /var/lib/apt/lists/*
9
+
10
+ # Copy requirements first for caching
11
+ COPY requirements.txt .
12
+ RUN pip install --no-cache-dir -r requirements.txt
13
+
14
+ # Copy application code
15
+ COPY . .
16
+
17
+ # Expose Streamlit port
18
+ EXPOSE 7860
19
+
20
+ # Set environment variables
21
+ ENV STREAMLIT_SERVER_PORT=7860
22
+ ENV STREAMLIT_SERVER_ADDRESS=0.0.0.0
23
+
24
+ # Run Streamlit
25
+ CMD ["streamlit", "run", "app.py", "--server.port=7860", "--server.address=0.0.0.0"]
README.md ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: FAR Chatbot
3
+ emoji: 🏛️
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: docker
7
+ pinned: false
8
+ license: mit
9
+ ---
10
+
11
+ # 🏛️ FAR Chatbot
12
+
13
+ An AI-powered assistant for the Federal Acquisition Regulation (FAR).
14
+
15
+ ## Features
16
+ - **RAG-powered answers**: Uses vector search to find relevant FAR sections
17
+ - **Clickable citations**: All FAR references link to acquisition.gov
18
+ - **GPT-4 Turbo**: Generates accurate, well-cited responses
19
+ - **Conversation memory**: Maintains context across questions
20
+
21
+ ## Login
22
+ - Username: `testuser`
23
+ - Password: `farbot2025`
24
+
25
+ ## How It Works
26
+ 1. Your question is converted to embeddings
27
+ 2. FAISS searches 3,893 FAR sections for relevant content
28
+ 3. GPT-4 Turbo generates a response using the actual FAR text
29
+ 4. Citations are linked to official acquisition.gov sources
30
+
31
+ ## Built With
32
+ - Sentence Transformers (paraphrase-MiniLM-L6-v2)
33
+ - FAISS vector database
34
+ - OpenAI GPT-4 Turbo
35
+ - Streamlit
app.py ADDED
@@ -0,0 +1,235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ FAR Chatbot - Hugging Face Spaces Version
4
+ Federal Acquisition Regulation Assistant with Clickable Citations
5
+ """
6
+
7
+ import streamlit as st
8
+ import time
9
+ import logging
10
+ import os
11
+ import sys
12
+ import re
13
+ from datetime import datetime
14
+
15
+ # Configure logging
16
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
17
+ logger = logging.getLogger(__name__)
18
+
19
+ # Import the chatbot
20
+ from far_chatbot import FARChatbot
21
+
22
+ try:
23
+ import markdown
24
+ except ImportError:
25
+ markdown = None
26
+
27
+ # Configure page
28
+ st.set_page_config(
29
+ page_title="FAR Chatbot - Federal Acquisition Regulation Assistant",
30
+ page_icon="🏛️",
31
+ layout="wide",
32
+ initial_sidebar_state="expanded"
33
+ )
34
+
35
+ # Authentication
36
+ VALID_CREDENTIALS = {"testuser": "farbot2025"}
37
+
38
+ def check_password():
39
+ def password_entered():
40
+ username = st.session_state.get("username", "")
41
+ password = st.session_state.get("password", "")
42
+ if username in VALID_CREDENTIALS and VALID_CREDENTIALS[username] == password:
43
+ st.session_state["authenticated"] = True
44
+ del st.session_state["password"]
45
+ else:
46
+ st.session_state["authenticated"] = False
47
+
48
+ if "authenticated" not in st.session_state:
49
+ st.session_state["authenticated"] = False
50
+
51
+ if not st.session_state["authenticated"]:
52
+ col1, col2, col3 = st.columns([1, 2, 1])
53
+ with col2:
54
+ st.markdown("## 🏛️ FAR Chatbot Login")
55
+ st.text_input("Username", key="username")
56
+ st.text_input("Password", type="password", key="password")
57
+ st.button("🔐 Log In", on_click=password_entered, type="primary", use_container_width=True)
58
+ if st.session_state.get("authenticated") == False and "username" in st.session_state and st.session_state["username"]:
59
+ st.error("❌ Invalid credentials")
60
+ return False
61
+ return True
62
+
63
+ if not check_password():
64
+ st.stop()
65
+
66
+ # CSS Styling
67
+ st.markdown("""
68
+ <style>
69
+ .main-header {
70
+ text-align: center; padding: 1.5rem;
71
+ background: linear-gradient(135deg, #1a365d 0%, #2c5282 50%, #2b6cb0 100%);
72
+ color: white; border-radius: 16px; margin-bottom: 2rem;
73
+ }
74
+ .main-header h1 { margin: 0; font-size: 2.5rem; }
75
+ .main-header p { margin: 0.5rem 0 0 0; opacity: 0.9; }
76
+ .user-message {
77
+ background: #f7fafc; padding: 1rem; border-radius: 16px;
78
+ margin: 1rem 0; border-left: 4px solid #e53e3e;
79
+ }
80
+ .bot-message {
81
+ background: linear-gradient(135deg, #ebf8ff 0%, #e6fffa 100%);
82
+ padding: 1.25rem; border-radius: 16px; margin: 1rem 0;
83
+ border-left: 4px solid #2b6cb0;
84
+ }
85
+ .citation-link {
86
+ background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
87
+ padding: 2px 8px; border-radius: 6px; font-family: monospace;
88
+ font-weight: 600; color: #92400e; text-decoration: none;
89
+ border: 1px solid #f59e0b;
90
+ }
91
+ .citation-link:hover { background: #fde68a; }
92
+ .source-card {
93
+ background: white; border: 1px solid #e2e8f0;
94
+ border-radius: 12px; padding: 1rem; margin: 0.75rem 0;
95
+ }
96
+ </style>
97
+ """, unsafe_allow_html=True)
98
+
99
+ # Session state
100
+ if 'chatbot' not in st.session_state:
101
+ st.session_state.chatbot = None
102
+ if 'chat_history' not in st.session_state:
103
+ st.session_state.chat_history = []
104
+ if 'show_sources' not in st.session_state:
105
+ st.session_state.show_sources = True
106
+
107
+ @st.cache_resource
108
+ def load_chatbot():
109
+ """Load the FAR chatbot"""
110
+ try:
111
+ logger.info("Loading FAR Chatbot...")
112
+ chatbot = FARChatbot(
113
+ faiss_index_path="data/faiss_index.index",
114
+ texts_path="data/texts.txt",
115
+ use_gpt5=True
116
+ )
117
+ return chatbot
118
+ except Exception as e:
119
+ logger.error(f"Error loading chatbot: {e}")
120
+ st.error(f"Error loading chatbot: {e}")
121
+ return None
122
+
123
+ def get_acquisition_gov_url(citation: str) -> str:
124
+ base_citation = re.sub(r'\([a-z]\)$', '', citation)
125
+ return f"https://www.acquisition.gov/far/{base_citation}"
126
+
127
+ def make_citations_clickable(response: str, search_results: list) -> str:
128
+ if not markdown:
129
+ return response
130
+
131
+ text = response
132
+
133
+ def make_link(citation):
134
+ url = get_acquisition_gov_url(citation)
135
+ return f'<a href="{url}" target="_blank" class="citation-link">'
136
+
137
+ # Replace citation patterns
138
+ text = re.sub(r'\[FAR\s+(\d+\.\d+(?:-\d+)?(?:\([a-z]\))?)\]',
139
+ lambda m: f'{make_link(m.group(1))}[{m.group(1)}]</a>', text)
140
+ text = re.sub(r'\[(\d+\.\d+(?:-\d+)?(?:\([a-z]\))?)\]',
141
+ lambda m: f'{make_link(m.group(1))}[{m.group(1)}]</a>', text)
142
+ text = re.sub(r'(?<!View )FAR\s+(\d+\.\d+(?:-\d+)?(?:\([a-z]\))?)',
143
+ lambda m: f'{make_link(m.group(1))}FAR {m.group(1)}</a>', text)
144
+
145
+ return markdown.markdown(text, extensions=['tables', 'fenced_code', 'nl2br'])
146
+
147
+ # Header
148
+ st.markdown("""
149
+ <div class="main-header">
150
+ <h1>🏛️ FAR Chatbot</h1>
151
+ <p>Federal Acquisition Regulation Assistant</p>
152
+ </div>
153
+ """, unsafe_allow_html=True)
154
+
155
+ # Sidebar
156
+ with st.sidebar:
157
+ st.markdown("## ⚙️ Settings")
158
+
159
+ if st.session_state.chatbot is None:
160
+ st.session_state.chatbot = load_chatbot()
161
+
162
+ if st.session_state.chatbot:
163
+ st.success("✅ Chatbot Ready!")
164
+ else:
165
+ st.error("❌ Failed to load")
166
+
167
+ st.session_state.show_sources = st.checkbox("Show sources", value=True)
168
+
169
+ st.markdown("### 💡 Sample Questions")
170
+ samples = [
171
+ "What are small business set-asides?",
172
+ "Explain the simplified acquisition threshold",
173
+ "What is the micro-purchase threshold?",
174
+ "When can I use sole source?"
175
+ ]
176
+ for q in samples:
177
+ if st.button(f"💬 {q}", key=f"s_{hash(q)}"):
178
+ st.session_state.current_question = q
179
+
180
+ if st.button("🗑️ Clear Chat"):
181
+ st.session_state.chat_history = []
182
+ st.rerun()
183
+
184
+ if st.button("🚪 Logout"):
185
+ st.session_state["authenticated"] = False
186
+ st.rerun()
187
+
188
+ # Main content
189
+ if st.session_state.chatbot is None:
190
+ st.error("Chatbot not loaded")
191
+ st.stop()
192
+
193
+ # Display chat history
194
+ for entry in st.session_state.chat_history:
195
+ question, answer, search_results, timestamp = entry[:4]
196
+
197
+ st.markdown(f'<div class="user-message">👤 <b>You:</b> {question}</div>', unsafe_allow_html=True)
198
+
199
+ formatted = make_citations_clickable(answer, search_results)
200
+ st.markdown(f'<div class="bot-message">🤖 <b>FAR Bot:</b><br>{formatted}</div>', unsafe_allow_html=True)
201
+
202
+ if st.session_state.show_sources and search_results:
203
+ with st.expander("📚 Sources"):
204
+ for cit, txt in search_results[:5]:
205
+ url = get_acquisition_gov_url(cit)
206
+ st.markdown(f"**[FAR {cit}]({url})**: {txt[:300]}...")
207
+
208
+ # Input
209
+ st.markdown("## 💬 Ask a Question")
210
+
211
+ question = ""
212
+ if 'current_question' in st.session_state:
213
+ question = st.session_state.current_question
214
+ del st.session_state.current_question
215
+
216
+ question = st.text_input("Your question:", value=question, placeholder="Ask about FAR regulations...")
217
+
218
+ if st.button("🚀 Ask", type="primary") and question.strip():
219
+ with st.spinner("Processing..."):
220
+ try:
221
+ result = st.session_state.chatbot.chat(question, top_k=None)
222
+
223
+ timestamp = datetime.now().strftime("%H:%M")
224
+ st.session_state.chat_history.append((
225
+ question,
226
+ result['response'],
227
+ result.get('search_results', []),
228
+ timestamp
229
+ ))
230
+ st.rerun()
231
+ except Exception as e:
232
+ st.error(f"Error: {e}")
233
+
234
+ st.markdown("---")
235
+ st.markdown("🏛️ FAR Chatbot | Powered by GPT-4 Turbo | [acquisition.gov](https://acquisition.gov)")
data/faiss_index.index ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:cf2c5198eeb91c146435dc22a65f48f25379238ca3b4a28ce368c1f0a93eab65
3
+ size 5979693
data/texts.txt ADDED
The diff for this file is too large to render. See raw diff
 
far_chatbot.py ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ FAR Chatbot - Simplified version for Hugging Face Spaces
4
+ """
5
+
6
+ import os
7
+ import logging
8
+ import numpy as np
9
+ import faiss
10
+ from sentence_transformers import SentenceTransformer
11
+ from openai import OpenAI
12
+ from typing import List, Tuple, Dict, Optional
13
+
14
+ logging.basicConfig(level=logging.INFO)
15
+ logger = logging.getLogger(__name__)
16
+
17
+ class ConversationMemory:
18
+ """Simple conversation memory"""
19
+ def __init__(self, max_turns: int = 5):
20
+ self.history = []
21
+ self.max_turns = max_turns
22
+ self.current_topics = []
23
+
24
+ def add_turn(self, question: str, answer: str, topics: List[str] = None):
25
+ self.history.append({"question": question, "answer": answer})
26
+ if len(self.history) > self.max_turns:
27
+ self.history.pop(0)
28
+ if topics:
29
+ self.current_topics = topics[:3]
30
+
31
+ def get_context(self) -> str:
32
+ if not self.history:
33
+ return ""
34
+ context = "Previous conversation:\n"
35
+ for turn in self.history[-3:]:
36
+ context += f"Q: {turn['question']}\nA: {turn['answer'][:200]}...\n\n"
37
+ return context
38
+
39
+ class FARChatbot:
40
+ """FAR Chatbot with RAG capabilities"""
41
+
42
+ def __init__(self, faiss_index_path: str, texts_path: str,
43
+ model_name: str = 'paraphrase-MiniLM-L6-v2',
44
+ openai_api_key: str = None, use_gpt5: bool = True):
45
+
46
+ self.use_gpt5 = use_gpt5
47
+ logger.info("Loading SentenceTransformer model...")
48
+ self.model = SentenceTransformer(model_name)
49
+ logger.info("Model loaded!")
50
+
51
+ # Load FAISS index
52
+ logger.info(f"Loading FAISS index from {faiss_index_path}")
53
+ self.faiss_index = faiss.read_index(faiss_index_path)
54
+ logger.info(f"FAISS index loaded with {self.faiss_index.ntotal} vectors")
55
+
56
+ # Load texts
57
+ logger.info(f"Loading texts from {texts_path}")
58
+ with open(texts_path, 'r', encoding='utf-8') as f:
59
+ self.texts = [line.strip() for line in f if line.strip()]
60
+ logger.info(f"Loaded {len(self.texts)} text chunks")
61
+
62
+ # OpenAI client
63
+ api_key = openai_api_key or os.getenv('OPENAI_API_KEY')
64
+ if not api_key:
65
+ raise ValueError("OpenAI API key required")
66
+ self.client = OpenAI(api_key=api_key)
67
+
68
+ self.conversation = ConversationMemory()
69
+
70
+ def search(self, query: str, top_k: int = 10) -> List[Tuple[str, str]]:
71
+ """Search for relevant FAR sections"""
72
+ query_embedding = self.model.encode([query])
73
+ distances, indices = self.faiss_index.search(query_embedding.astype('float32'), top_k)
74
+
75
+ results = []
76
+ for idx in indices[0]:
77
+ if 0 <= idx < len(self.texts):
78
+ text = self.texts[idx]
79
+ # Extract citation from text
80
+ citation = "Unknown"
81
+ if text.startswith("FAR "):
82
+ parts = text.split(":", 1)
83
+ if len(parts) > 1:
84
+ citation = parts[0].replace("FAR ", "").strip()
85
+ results.append((citation, text))
86
+
87
+ return results
88
+
89
+ def chat(self, question: str, top_k: int = None) -> Dict:
90
+ """Process a question and return response"""
91
+
92
+ # Determine context size
93
+ actual_top_k = 50 if self.use_gpt5 else (top_k or 10)
94
+
95
+ # Search for relevant content
96
+ search_results = self.search(question, top_k=actual_top_k)
97
+
98
+ # Build context
99
+ context = "\n\n".join([f"[{cit}]: {txt[:800]}" for cit, txt in search_results[:20]])
100
+
101
+ # Get conversation history
102
+ conv_context = self.conversation.get_context()
103
+
104
+ # Build prompt
105
+ system_prompt = """You are FAR Bot, an expert assistant for the Federal Acquisition Regulation (FAR).
106
+
107
+ INSTRUCTIONS:
108
+ 1. Answer questions accurately based on the FAR content provided
109
+ 2. ALWAYS cite specific FAR sections using [X.XXX] format
110
+ 3. Be concise but thorough
111
+ 4. If information isn't in the context, say so
112
+ 5. Suggest follow-up questions when appropriate"""
113
+
114
+ user_prompt = f"""Question: {question}
115
+
116
+ {conv_context}
117
+
118
+ Relevant FAR Sections:
119
+ {context}
120
+
121
+ Provide a clear, well-cited answer:"""
122
+
123
+ # Call OpenAI
124
+ try:
125
+ response = self.client.chat.completions.create(
126
+ model="gpt-4-turbo-preview",
127
+ messages=[
128
+ {"role": "system", "content": system_prompt},
129
+ {"role": "user", "content": user_prompt}
130
+ ],
131
+ temperature=0.3,
132
+ max_tokens=1500
133
+ )
134
+ answer = response.choices[0].message.content
135
+ except Exception as e:
136
+ logger.error(f"OpenAI error: {e}")
137
+ answer = f"Error generating response: {e}"
138
+
139
+ # Extract topics (simple extraction)
140
+ topics = []
141
+ topic_keywords = ["small business", "threshold", "competition", "contract", "bid", "proposal"]
142
+ for kw in topic_keywords:
143
+ if kw.lower() in question.lower():
144
+ topics.append(kw.title())
145
+
146
+ # Update conversation
147
+ self.conversation.add_turn(question, answer, topics)
148
+
149
+ # Generate suggestions
150
+ suggestions = [
151
+ f"What are the exceptions to this rule?",
152
+ f"Can you provide more details about the thresholds?",
153
+ f"What documentation is required?"
154
+ ]
155
+
156
+ return {
157
+ 'response': answer,
158
+ 'suggestions': suggestions[:3],
159
+ 'topics': topics,
160
+ 'sections': [cit for cit, _ in search_results[:5]],
161
+ 'search_results': search_results[:10],
162
+ 'context_size': len(search_results),
163
+ 'model_used': 'gpt-4-turbo'
164
+ }
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ sentence-transformers>=2.2.0
2
+ faiss-cpu>=1.7.0
3
+ openai>=1.0.0
4
+ numpy>=1.21.0
5
+ streamlit>=1.28.0
6
+ markdown>=3.4.0