File size: 5,610 Bytes
fece80e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d7986f8
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
# rag_engine.py

import os
import warnings
from typing import List
from dotenv import load_dotenv

# Suppress warnings
warnings.filterwarnings('ignore')

# Set environment variables before imports
os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE"
os.environ["USER_AGENT"] = "PythonicAI/1.0 (+adinafraz01@gmail.com)"
os.environ["TOKENIZERS_PARALLELISM"] = "false"

# Load env variables
load_dotenv()
openai_key = os.getenv("OPENAI_API_KEY")

if not openai_key:
    raise ValueError("OPENAI_API_KEY not found in environment variables. Please create a .env file with your API key.")

os.environ["OPENAI_API_KEY"] = openai_key

from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_community.vectorstores import FAISS
from langchain_community.document_loaders import WebBaseLoader
from langchain_core.prompts import PromptTemplate
from langchain.schema import Document

# ------------------------------------------
# DOCUMENT LOADING (URL ONLY)
# ------------------------------------------

def load_user_document(url: str) -> List[Document]:
    """Load URL into LangChain documents."""
    try:
        if not url:
            raise ValueError("No URL provided")
        
        # Clean URL
        if not url.startswith(('http://', 'https://')):
            url = f'https://{url}'
            
        # Use WebBaseLoader with custom headers
        headers = {
            'User-Agent': os.environ.get("USER_AGENT", "PythonicAI/1.0")
        }
        
        loader = WebBaseLoader(
            web_paths=[url],
            requests_per_second=1,
            requests_kwargs={'headers': headers}
        )
        docs = loader.load()
        
        if not docs:
            raise ValueError("No content found in the URL")
            
        return docs
        
    except Exception as e:
        raise Exception(f"Error loading URL: {str(e)}")

# ------------------------------------------
# BUILD RETRIEVER FROM DOCUMENTS
# ------------------------------------------

def build_retriever_from_docs(docs: List[Document]):
    """Build FAISS vector store from documents."""
    if not docs:
        raise ValueError("No documents provided")
    
    # Use the same splitter settings as your example
    splitter = RecursiveCharacterTextSplitter(
        chunk_size=1000,
        chunk_overlap=200
    )
    
    chunks = splitter.split_documents(docs)
    
    # Use the same embedding model as your example
    embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
    
    # Build vector store
    vector_store = FAISS.from_documents(chunks, embeddings)
    
    return vector_store.as_retriever(
        search_type="similarity",
        search_kwargs={"k": 6}
    )

# ------------------------------------------
# PYTHON DOMAIN CHECK
# ------------------------------------------

_llm = None

def get_llm():
    """Singleton pattern for LLM to avoid re-initialization."""
    global _llm
    if _llm is None:
        _llm = ChatOpenAI(
            model="gpt-4o-mini",
            temperature=0.2,
            max_tokens=1000,
            request_timeout=60
        )
    return _llm

def is_python_related(question: str) -> bool:
    """Check if question is Python-related."""
    python_keywords = [
        'python', 'numpy', 'pandas', 'matplotlib', 'seaborn',
        'machine learning', 'deep learning', 'tensorflow', 'pytorch',
        'scikit-learn', 'sklearn', 'dataframe', 'array', 'plot',
        'import', 'def ', 'class ', 'function', 'method', 'library',
        'package', 'module', 'pip install', 'conda install', 'jupyter',
        'data', 'science', 'analysis', 'visualization', 'programming',
        'code', 'script', 'algorithm', 'model', 'training', 'prediction'
    ]
    
    # Quick keyword check first
    question_lower = question.lower()
    for keyword in python_keywords:
        if keyword in question_lower:
            return True
    
    return False

# ------------------------------------------
# ANSWER QUESTION
# ------------------------------------------

# Define the prompt template (same as your example)
prompt = PromptTemplate(
    template="""
    You are a helpful python assistant. 
    Answer ONLY from the provided text context.
    Display answer in a beautiful and user understandable format with proper formatting.
    Use bullet points, numbered lists, or code blocks when appropriate.
    If the context is insufficient, just say you don't know.

    Context: {context}
    Question: {question}
    """,
    input_variables=['context', 'question']
)

def answer_question(question: str, retriever):
    """Answer question using RAG pipeline."""
    # Validate topic
    if not is_python_related(question):
        return "I can only answer Python and data science related questions based on the provided documentation."
    
    try:
        # Retrieve relevant documents
        docs = retriever.invoke(question)
        
        if not docs:
            return "I couldn't find relevant information in the documentation to answer this question."
        
        # Combine context from retrieved documents
        context = "\n\n".join(doc.page_content for doc in docs)
        
        # Create final prompt
        final_prompt = prompt.invoke({
            "context": context,
            "question": question
        })
        
        # Get answer from LLM
        llm = get_llm()
        answer = llm.invoke(final_prompt)
        
        return answer.content
        
    except Exception as e:
        return f"Error generating answer: {str(e)}"