Spaces:
Sleeping
Sleeping
| #!/usr/bin/env python3 | |
| """ | |
| FAR Chatbot - Simplified version for Hugging Face Spaces | |
| """ | |
| import os | |
| import logging | |
| import numpy as np | |
| import faiss | |
| from sentence_transformers import SentenceTransformer | |
| from openai import OpenAI | |
| from typing import List, Tuple, Dict, Optional | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger(__name__) | |
| class ConversationMemory: | |
| """Simple conversation memory""" | |
| def __init__(self, max_turns: int = 5): | |
| self.history = [] | |
| self.max_turns = max_turns | |
| self.current_topics = [] | |
| def add_turn(self, question: str, answer: str, topics: List[str] = None): | |
| self.history.append({"question": question, "answer": answer}) | |
| if len(self.history) > self.max_turns: | |
| self.history.pop(0) | |
| if topics: | |
| self.current_topics = topics[:3] | |
| def get_context(self) -> str: | |
| if not self.history: | |
| return "" | |
| context = "Previous conversation:\n" | |
| for turn in self.history[-3:]: | |
| context += f"Q: {turn['question']}\nA: {turn['answer'][:200]}...\n\n" | |
| return context | |
| class FARChatbot: | |
| """FAR Chatbot with RAG capabilities""" | |
| def __init__(self, faiss_index_path: str, texts_path: str, | |
| model_name: str = 'paraphrase-MiniLM-L6-v2', | |
| openai_api_key: str = None, use_gpt5: bool = True): | |
| self.use_gpt5 = use_gpt5 | |
| logger.info("Loading SentenceTransformer model...") | |
| self.model = SentenceTransformer(model_name) | |
| logger.info("Model loaded!") | |
| # Load FAISS index | |
| logger.info(f"Loading FAISS index from {faiss_index_path}") | |
| self.faiss_index = faiss.read_index(faiss_index_path) | |
| logger.info(f"FAISS index loaded with {self.faiss_index.ntotal} vectors") | |
| # Load texts | |
| logger.info(f"Loading texts from {texts_path}") | |
| with open(texts_path, 'r', encoding='utf-8') as f: | |
| self.texts = [line.strip() for line in f if line.strip()] | |
| logger.info(f"Loaded {len(self.texts)} text chunks") | |
| # OpenAI client | |
| api_key = openai_api_key or os.getenv('OPENAI_API_KEY') | |
| if not api_key: | |
| raise ValueError("OpenAI API key required") | |
| self.client = OpenAI(api_key=api_key) | |
| self.conversation = ConversationMemory() | |
| def search(self, query: str, top_k: int = 10) -> List[Tuple[str, str]]: | |
| """Search for relevant FAR sections""" | |
| query_embedding = self.model.encode([query]) | |
| distances, indices = self.faiss_index.search(query_embedding.astype('float32'), top_k) | |
| results = [] | |
| for idx in indices[0]: | |
| if 0 <= idx < len(self.texts): | |
| text = self.texts[idx] | |
| # Extract citation from text | |
| citation = "Unknown" | |
| if text.startswith("FAR "): | |
| parts = text.split(":", 1) | |
| if len(parts) > 1: | |
| citation = parts[0].replace("FAR ", "").strip() | |
| results.append((citation, text)) | |
| return results | |
| def chat(self, question: str, top_k: int = None) -> Dict: | |
| """Process a question and return response""" | |
| # Determine context size | |
| actual_top_k = 50 if self.use_gpt5 else (top_k or 10) | |
| # Search for relevant content | |
| search_results = self.search(question, top_k=actual_top_k) | |
| # Build context | |
| context = "\n\n".join([f"[{cit}]: {txt[:800]}" for cit, txt in search_results[:20]]) | |
| # Get conversation history | |
| conv_context = self.conversation.get_context() | |
| # Build prompt | |
| system_prompt = """You are FAR Bot, an expert assistant for the Federal Acquisition Regulation (FAR). | |
| INSTRUCTIONS: | |
| 1. Answer questions accurately based on the FAR content provided | |
| 2. ALWAYS cite specific FAR sections using [X.XXX] format | |
| 3. Be concise but thorough | |
| 4. If information isn't in the context, say so | |
| 5. Suggest follow-up questions when appropriate""" | |
| user_prompt = f"""Question: {question} | |
| {conv_context} | |
| Relevant FAR Sections: | |
| {context} | |
| Provide a clear, well-cited answer:""" | |
| # Call OpenAI | |
| try: | |
| response = self.client.chat.completions.create( | |
| model="gpt-4-turbo-preview", | |
| messages=[ | |
| {"role": "system", "content": system_prompt}, | |
| {"role": "user", "content": user_prompt} | |
| ], | |
| temperature=0.3, | |
| max_tokens=1500 | |
| ) | |
| answer = response.choices[0].message.content | |
| except Exception as e: | |
| logger.error(f"OpenAI error: {e}") | |
| answer = f"Error generating response: {e}" | |
| # Extract topics (simple extraction) | |
| topics = [] | |
| topic_keywords = ["small business", "threshold", "competition", "contract", "bid", "proposal"] | |
| for kw in topic_keywords: | |
| if kw.lower() in question.lower(): | |
| topics.append(kw.title()) | |
| # Update conversation | |
| self.conversation.add_turn(question, answer, topics) | |
| # Generate suggestions | |
| suggestions = [ | |
| f"What are the exceptions to this rule?", | |
| f"Can you provide more details about the thresholds?", | |
| f"What documentation is required?" | |
| ] | |
| return { | |
| 'response': answer, | |
| 'suggestions': suggestions[:3], | |
| 'topics': topics, | |
| 'sections': [cit for cit, _ in search_results[:5]], | |
| 'search_results': search_results[:10], | |
| 'context_size': len(search_results), | |
| 'model_used': 'gpt-4-turbo' | |
| } | |