Sangyog commited on
Commit
311d019
·
unverified ·
2 Parent(s): 72b7684b0dd1a2

Merge pull request #22 from cyberalertnepal/sangyog

Browse files
.env-example CHANGED
@@ -1,2 +1,34 @@
1
  MY_SECRET_TOKEN="SECRET_CODE_TOKEN"
2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  MY_SECRET_TOKEN="SECRET_CODE_TOKEN"
2
 
3
+ # CHROMA_HOST = "localhost" (Host gareko address rakhney)
4
+
5
+
6
+ # EXAMPLE CONFIGURATIONS FOR DIFFERENT PROVIDERS(Use only one at once)
7
+ # ===========================================
8
+
9
+ # FOR OPENAI:(PAID)
10
+ # LLM_PROVIDER=openai
11
+ # LLM_API_KEY=sk-your-openai-api-key
12
+ # LLM_MODEL=gpt-3.5-turbo
13
+ # # Other options: gpt-4, gpt-4-turbo-preview, etc.
14
+
15
+ # FOR GROQ:(FREE: BABAL XA-> prefer this)
16
+ # LLM_PROVIDER=groq
17
+ # LLM_API_KEY=gsk_your-groq-api-key
18
+ # LLM_MODEL=llama-3.3-70b-versatile
19
+ # # Other options: llama-3.1-70b-versatile, mixtral-8x7b-32768, etc.
20
+
21
+ # FOR OPENROUTER:(FREE: LASTAI RATE LIMIT LAGAUXA)
22
+ # LLM_PROVIDER=openrouter
23
+ # LLM_API_KEY=sk-or-your-openrouter-api-key
24
+ # LLM_MODEL=meta-llama/llama-3.1-8b-instruct:free
25
+ # # Other options: anthropic/claude-3-haiku, google/gemma-7b-it, etc.
26
+
27
+ # ===========================================
28
+ # ADVANCED CONFIGURATION
29
+ # ===========================================
30
+ # Temperature (0.0 to 1.0) - controls randomness
31
+ # LLM_TEMPERATURE=0.1
32
+
33
+ # Maximum tokens for response
34
+ # LLM_MAX_TOKENS=4096
.gitignore CHANGED
@@ -66,3 +66,6 @@ notebooks
66
  np_text_model/classifier/sentencepiece.bpe.model
67
  np_text_model/classifier/tokenizer.json
68
 
 
 
 
 
66
  np_text_model/classifier/sentencepiece.bpe.model
67
  np_text_model/classifier/tokenizer.json
68
 
69
+ # vector database
70
+ chroma_data
71
+ chroma_database
README.md CHANGED
@@ -119,7 +119,9 @@ AI-Checker/
119
  2. **Run the API**
120
 
121
  ```bash
122
- uvicorn app:app --reload
 
 
123
  ```
124
 
125
  3. **Build Docker (optional)**
 
119
  2. **Run the API**
120
 
121
  ```bash
122
+ chroma run --path ./chroma_database ## to run chromadb locally
123
+ uvicorn app:app --reload --port 8001 ## fastapi (run after chromadb)
124
+
125
  ```
126
 
127
  3. **Build Docker (optional)**
app.py CHANGED
@@ -11,6 +11,7 @@ from features.nepali_text_classifier.routes import (
11
  )
12
  from features.image_classifier.routes import router as image_classifier_router
13
  from features.image_edit_detector.routes import router as image_edit_detector_router
 
14
  from fastapi.staticfiles import StaticFiles
15
 
16
  from config import ACCESS_RATE
@@ -41,6 +42,8 @@ app.include_router(text_classifier_router, prefix="/text")
41
  app.include_router(nepali_text_classifier_router, prefix="/NP")
42
  app.include_router(image_classifier_router, prefix="/AI-image")
43
  app.include_router(image_edit_detector_router, prefix="/detect")
 
 
44
 
45
 
46
  @app.get("/")
 
11
  )
12
  from features.image_classifier.routes import router as image_classifier_router
13
  from features.image_edit_detector.routes import router as image_edit_detector_router
14
+ from features.rag_chatbot.routes import router as rag_router
15
  from fastapi.staticfiles import StaticFiles
16
 
17
  from config import ACCESS_RATE
 
42
  app.include_router(nepali_text_classifier_router, prefix="/NP")
43
  app.include_router(image_classifier_router, prefix="/AI-image")
44
  app.include_router(image_edit_detector_router, prefix="/detect")
45
+ app.include_router(rag_router, prefix="/rag")
46
+
47
 
48
 
49
  @app.get("/")
features/rag_chatbot/__init__.py ADDED
File without changes
features/rag_chatbot/controller.py ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import asyncio
3
+ import logging
4
+ from io import BytesIO
5
+ from typing import Dict, Any
6
+
7
+ from fastapi import HTTPException, UploadFile, status, Depends
8
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
9
+
10
+ from .rag_pipeline import route_and_process_query, add_document_to_rag, check_system_health
11
+ from .document_handler import extract_text_from_file
12
+
13
+ # Configure logging
14
+ logging.basicConfig(level=logging.INFO)
15
+ logger = logging.getLogger(__name__)
16
+
17
+ security = HTTPBearer()
18
+
19
+ # Supported file types
20
+ SUPPORTED_CONTENT_TYPES = {
21
+ "application/pdf",
22
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
23
+ "text/plain"
24
+ }
25
+
26
+ MAX_FILE_SIZE = 100 * 1024 * 1024 # 100MB
27
+
28
+ async def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
29
+ """Verify Bearer token from Authorization header."""
30
+ token = credentials.credentials
31
+ expected_token = os.getenv("MY_SECRET_TOKEN")
32
+
33
+ if not expected_token:
34
+ logger.error("MY_SECRET_TOKEN not configured")
35
+ raise HTTPException(
36
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
37
+ detail="Server configuration error"
38
+ )
39
+
40
+ if token != expected_token:
41
+ logger.warning(f"Invalid token attempt: {token[:10]}...")
42
+ raise HTTPException(
43
+ status_code=status.HTTP_403_FORBIDDEN,
44
+ detail="Invalid or expired token"
45
+ )
46
+ return token
47
+
48
+ async def handle_rag_query(query: str) -> Dict[str, Any]:
49
+ """Handle an incoming query by routing it and getting the appropriate answer."""
50
+
51
+ # Input validation
52
+ if not query or not query.strip():
53
+ raise HTTPException(
54
+ status_code=status.HTTP_400_BAD_REQUEST,
55
+ detail="Query cannot be empty"
56
+ )
57
+
58
+ if len(query) > 1000: # Reasonable limit
59
+ raise HTTPException(
60
+ status_code=status.HTTP_400_BAD_REQUEST,
61
+ detail="Query too long. Please limit to 1000 characters."
62
+ )
63
+
64
+ try:
65
+ logger.info(f"Processing query: {query[:50]}...")
66
+
67
+ # Process query in thread pool
68
+ response = await asyncio.to_thread(route_and_process_query, query)
69
+
70
+ logger.info(f"Query processed successfully. Route: {response.get('route', 'Unknown')}")
71
+ return response
72
+
73
+ except Exception as e:
74
+ logger.error(f"Error processing query: {e}")
75
+ raise HTTPException(
76
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
77
+ detail="Error processing your query. Please try again."
78
+ )
79
+
80
+ async def handle_document_upload(file: UploadFile) -> Dict[str, str]:
81
+ """Handle uploading a document to the RAG's vector store."""
82
+
83
+ # File validation
84
+ if not file.filename:
85
+ raise HTTPException(
86
+ status_code=status.HTTP_400_BAD_REQUEST,
87
+ detail="No file provided"
88
+ )
89
+
90
+ if file.content_type not in SUPPORTED_CONTENT_TYPES:
91
+ raise HTTPException(
92
+ status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
93
+ detail=f"Unsupported file type: {file.content_type}. "
94
+ f"Supported types: {', '.join(SUPPORTED_CONTENT_TYPES)}"
95
+ )
96
+
97
+ # Check file size
98
+ contents = await file.read()
99
+ if len(contents) > MAX_FILE_SIZE:
100
+ raise HTTPException(
101
+ status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
102
+ detail=f"File too large. Maximum size: {MAX_FILE_SIZE / (1024*1024):.1f}MB"
103
+ )
104
+
105
+ # Reset file pointer
106
+ await file.seek(0)
107
+
108
+ try:
109
+ logger.info(f"Processing file upload: {file.filename}")
110
+
111
+ # Extract text from file
112
+ text = await extract_text_from_file(file)
113
+
114
+ if not text or not text.strip():
115
+ raise HTTPException(
116
+ status_code=status.HTTP_400_BAD_REQUEST,
117
+ detail="The file appears to be empty or could not be read."
118
+ )
119
+
120
+ if len(text) < 50: # Too short to be meaningful
121
+ raise HTTPException(
122
+ status_code=status.HTTP_400_BAD_REQUEST,
123
+ detail="The extracted text is too short to be meaningful."
124
+ )
125
+
126
+ # Add to RAG system
127
+ success = await asyncio.to_thread(
128
+ add_document_to_rag,
129
+ text,
130
+ {
131
+ "source": file.filename,
132
+ "content_type": file.content_type,
133
+ "size": len(contents)
134
+ }
135
+ )
136
+
137
+ if not success:
138
+ raise HTTPException(
139
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
140
+ detail="Failed to add document to the knowledge base"
141
+ )
142
+
143
+ logger.info(f"Successfully processed file: {file.filename}")
144
+
145
+ return {
146
+ "message": f"Successfully uploaded and processed '{file.filename}'. "
147
+ f"It is now available for querying.",
148
+ "filename": file.filename,
149
+ "text_length": len(text),
150
+ "content_type": file.content_type
151
+ }
152
+
153
+ except HTTPException:
154
+ raise
155
+ except Exception as e:
156
+ logger.error(f"Error processing file {file.filename}: {e}")
157
+ raise HTTPException(
158
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
159
+ detail="Error processing the file. Please try again."
160
+ )
161
+
162
+ async def handle_health_check() -> Dict[str, Any]:
163
+ """Handle health check requests."""
164
+ try:
165
+ health_status = await asyncio.to_thread(check_system_health)
166
+
167
+ if health_status["status"] == "unhealthy":
168
+ raise HTTPException(
169
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
170
+ detail="Service is currently unhealthy"
171
+ )
172
+
173
+ return health_status
174
+
175
+ except HTTPException:
176
+ raise
177
+ except Exception as e:
178
+ logger.error(f"Health check failed: {e}")
179
+ raise HTTPException(
180
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
181
+ detail="Health check failed"
182
+ )
features/rag_chatbot/document_handler.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from io import BytesIO
2
+ from fastapi import UploadFile, HTTPException
3
+ import PyPDF2
4
+ import docx
5
+
6
+ async def extract_text_from_file(file: UploadFile) -> str:
7
+ """Extracts text from various file types."""
8
+ content = await file.read()
9
+ file_stream = BytesIO(content)
10
+
11
+ if file.content_type == "application/pdf":
12
+ return extract_text_from_pdf(file_stream)
13
+ elif file.content_type == "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
14
+ return extract_text_from_docx(file_stream)
15
+ elif file.content_type == "text/plain":
16
+ return file_stream.read().decode("utf-8")
17
+ else:
18
+ raise HTTPException(
19
+ status_code=415,
20
+ detail="Unsupported file type. Please upload a .pdf, .docx, or .txt file."
21
+ )
22
+
23
+ def extract_text_from_pdf(file_stream: BytesIO) -> str:
24
+ """Extracts text from a PDF file."""
25
+ reader = PyPDF2.PdfReader(file_stream)
26
+ text = ""
27
+ for page in reader.pages:
28
+ text += page.extract_text() or ""
29
+ return text
30
+
31
+ def extract_text_from_docx(file_stream: BytesIO) -> str:
32
+ """Extracts text from a DOCX file."""
33
+ doc = docx.Document(file_stream)
34
+ text = ""
35
+ for para in doc.paragraphs:
36
+ text += para.text + "\n"
37
+ return text
features/rag_chatbot/rag_pipeline.py ADDED
@@ -0,0 +1,327 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import chromadb
3
+ from dotenv import load_dotenv
4
+ from langchain_core.documents import Document
5
+ from langchain.text_splitter import RecursiveCharacterTextSplitter
6
+ from langchain_community.embeddings import HuggingFaceEmbeddings
7
+ from langchain_community.llms import OpenAI
8
+ from langchain.chains.question_answering import load_qa_chain
9
+ from langchain_community.vectorstores import Chroma
10
+ from langchain.chains import LLMChain
11
+ from langchain.prompts import PromptTemplate
12
+ from langchain.chat_models import ChatOpenAI
13
+
14
+
15
+ load_dotenv()
16
+
17
+ # ChromaDB configuration
18
+ CHROMA_HOST = os.getenv("CHROMA_HOST", "localhost") # change in env in production when hosted
19
+ COLLECTION_NAME = "company_docs_collection"
20
+
21
+ # LLM Provider Configuration
22
+ LLM_PROVIDER = os.getenv("LLM_PROVIDER", "openai").lower()
23
+ LLM_API_KEY = os.getenv("LLM_API_KEY")
24
+ LLM_MODEL = os.getenv("LLM_MODEL", "gpt-3.5-turbo")
25
+ LLM_TEMPERATURE = float(os.getenv("LLM_TEMPERATURE", "0"))
26
+ LLM_MAX_TOKENS = int(os.getenv("LLM_MAX_TOKENS", "2048"))
27
+
28
+ # Provider-specific configurations
29
+ PROVIDER_CONFIGS = {
30
+ "openai": {
31
+ "api_base": "https://api.openai.com/v1",
32
+ "default_model": "gpt-3.5-turbo"
33
+ },
34
+ "groq": {
35
+ "api_base": "https://api.groq.com/openai/v1",
36
+ "default_model": "llama-3.3-70b-versatile"
37
+ },
38
+ "openrouter": {
39
+ "api_base": "https://openrouter.ai/api/v1",
40
+ "default_model": "mistralai/mistral-small-3.2-24b-instruct:free"
41
+ }
42
+ }
43
+
44
+ vector_store = None
45
+ company_qa_chain = None
46
+ query_router_chain = None
47
+ cybersecurity_chain = None
48
+ llm = None
49
+
50
+ def get_llm_config():
51
+ """Get the appropriate LLM configuration based on the provider."""
52
+ if LLM_PROVIDER not in PROVIDER_CONFIGS:
53
+ raise ValueError(f"Unsupported LLM provider: {LLM_PROVIDER}. Supported: {list(PROVIDER_CONFIGS.keys())}")
54
+
55
+ config = PROVIDER_CONFIGS[LLM_PROVIDER].copy()
56
+
57
+ # Use provided model or fall back to default
58
+ model = LLM_MODEL if LLM_MODEL != "gpt-3.5-turbo" else config["default_model"]
59
+
60
+ return {
61
+ "model": model,
62
+ "openai_api_key": LLM_API_KEY,
63
+ "openai_api_base": config["api_base"],
64
+ "temperature": LLM_TEMPERATURE,
65
+ "max_tokens": LLM_MAX_TOKENS,
66
+ }
67
+
68
+ def initialize_llm():
69
+ """Initialize the LLM based on the configured provider."""
70
+ if not LLM_API_KEY:
71
+ raise ValueError(f"LLM_API_KEY environment variable is required for {LLM_PROVIDER}")
72
+
73
+ config = get_llm_config()
74
+
75
+ print(f"Initializing {LLM_PROVIDER.upper()} with model: {config['model']}")
76
+
77
+ return ChatOpenAI(**config)
78
+
79
+ def initialize_pipelines():
80
+ """Initializes all required models, chains, and the vector store."""
81
+ global vector_store, company_qa_chain, query_router_chain, cybersecurity_chain, llm
82
+
83
+ try:
84
+ # Initialize LLM
85
+ llm = initialize_llm()
86
+
87
+ # Initialize embeddings
88
+ embeddings = HuggingFaceEmbeddings(
89
+ model_name="all-MiniLM-L6-v2",
90
+ model_kwargs={'device': 'cpu'},
91
+ encode_kwargs={'normalize_embeddings': True}
92
+ )
93
+
94
+ # Initialize ChromaDB client
95
+ try:
96
+ chroma_client = chromadb.HttpClient(host=CHROMA_HOST, port=8000)
97
+ chroma_client.heartbeat()
98
+ except Exception as e:
99
+ raise ConnectionError("Failed to connect to ChromaDB.") from e
100
+
101
+ # Initialize vector store
102
+ vector_store = Chroma(
103
+ client=chroma_client,
104
+ collection_name=COLLECTION_NAME,
105
+ embedding_function=embeddings,
106
+ )
107
+
108
+ # Query Router Chain
109
+ router_template = """You are a query classifier. Classify the following query into one of these categories:
110
+ - COMPANY: Questions about our company, its products, services, or general information
111
+ - CYBERSECURITY: Questions about cybersecurity, security threats, best practices, or vulnerabilities
112
+ - OFF_TOPIC: Questions that don't fit the above categories
113
+
114
+ Query: {query}
115
+
116
+ Respond with only the category name (COMPANY, CYBERSECURITY, or OFF_TOPIC):"""
117
+
118
+ router_prompt = PromptTemplate(
119
+ input_variables=["query"],
120
+ template=router_template
121
+ )
122
+
123
+ query_router_chain = LLMChain(
124
+ llm=llm,
125
+ prompt=router_prompt
126
+ )
127
+
128
+ # Custom Company QA Chain
129
+ company_qa_template = """You are a helpful assistant for CyberAlertNepal. Answer the following question about our company using the information provided and links if only available. Give a natural, direct and polite response.
130
+
131
+ Question: {question}
132
+
133
+ Information:
134
+ {context}
135
+
136
+ Answer:"""
137
+
138
+ company_qa_prompt = PromptTemplate(
139
+ input_variables=["question", "context"],
140
+ template=company_qa_template
141
+ )
142
+
143
+ company_qa_chain = LLMChain(
144
+ llm=llm,
145
+ prompt=company_qa_prompt
146
+ )
147
+
148
+ # Cybersecurity Chain
149
+ cybersecurity_template = """You are a cybersecurity professional. Answer the following question truthfully and concisely.
150
+ If you are not 100% sure about the answer, simply respond with: "I am not sure about the answer."
151
+ Do not add extra explanations or assumptions. Do not provide false or speculative information.
152
+
153
+ Question: {question}
154
+
155
+ Provide a comprehensive and accurate answer about cybersecurity:"""
156
+
157
+ cybersecurity_prompt = PromptTemplate(
158
+ input_variables=["question"],
159
+ template=cybersecurity_template
160
+ )
161
+
162
+ cybersecurity_chain = LLMChain(
163
+ llm=llm,
164
+ prompt=cybersecurity_prompt
165
+ )
166
+
167
+ print(f"Successfully initialized pipelines with {LLM_PROVIDER.upper()}")
168
+
169
+ except Exception as e:
170
+ print(f"Error initializing pipelines: {e}")
171
+ raise
172
+
173
+ def add_document_to_rag(text: str, metadata: dict):
174
+ """Splits a document and adds it to the ChromaDB index."""
175
+ global vector_store
176
+
177
+ if not vector_store:
178
+ initialize_pipelines()
179
+
180
+ try:
181
+ text_splitter = RecursiveCharacterTextSplitter(
182
+ chunk_size=1000,
183
+ chunk_overlap=200
184
+ )
185
+ docs = text_splitter.create_documents([text], metadatas=[metadata])
186
+
187
+ if not docs:
188
+ print("Document was empty after splitting, not adding to ChromaDB.")
189
+ return False
190
+
191
+ vector_store.add_documents(docs)
192
+ print("Successfully added documents.")
193
+ return True
194
+
195
+ except Exception as e:
196
+ print(f"Error adding document to RAG: {e}")
197
+ return False
198
+
199
+ def route_and_process_query(query: str):
200
+ """Routes the query and processes it using the appropriate pipeline."""
201
+ global query_router_chain, vector_store, company_qa_chain, cybersecurity_chain
202
+
203
+ if not all([query_router_chain, vector_store, company_qa_chain, cybersecurity_chain]):
204
+ initialize_pipelines()
205
+
206
+ try:
207
+ # 1. Classify the query
208
+ route_result = query_router_chain.run(query)
209
+ route = route_result.strip().upper()
210
+
211
+
212
+ # 2. Route to appropriate logic
213
+ if "CYBERSECURITY" in route:
214
+ answer = cybersecurity_chain.run(question=query)
215
+ return {
216
+ "answer": answer,
217
+ "source": "Cybersecurity Knowledge Base",
218
+ "route": "CYBERSECURITY",
219
+ "provider": LLM_PROVIDER.upper(),
220
+ "model": get_llm_config()["model"]
221
+ }
222
+
223
+ elif "COMPANY" in route:
224
+ # Perform similarity search on ChromaDB
225
+ docs = vector_store.similarity_search(query, k=3)
226
+
227
+ if not docs:
228
+ return {
229
+ "answer": "I could not find any relevant information to answer your question.",
230
+ "source": "Company Documents",
231
+ "route": "COMPANY",
232
+ "provider": LLM_PROVIDER.upper(),
233
+ "model": get_llm_config()["model"]
234
+ }
235
+
236
+ # Combine document content for context
237
+ context = "\n\n".join([doc.page_content for doc in docs])
238
+
239
+ # Run the custom QA chain
240
+ answer = company_qa_chain.run(question=query, context=context)
241
+ sources = list(set([doc.metadata.get("source", "Unknown") for doc in docs]))
242
+
243
+ return {
244
+ "answer": answer,
245
+ "source": "Company Documents",
246
+ "documents": sources,
247
+ "route": "COMPANY",
248
+ "provider": LLM_PROVIDER.upper(),
249
+ "model": get_llm_config()["model"]
250
+ }
251
+
252
+ else: # OFF_TOPIC
253
+ return {
254
+ "answer": "I am a specialized assistant of CyberAlertNepal. I cannot answer questions outside of cybersecurity topics.",
255
+ "source": "N/A",
256
+ "route": "OFF_TOPIC",
257
+ "provider": LLM_PROVIDER.upper(),
258
+ "model": get_llm_config()["model"]
259
+ }
260
+
261
+ except Exception as e:
262
+ print(f"Error processing query: {e}")
263
+ return {
264
+ "answer": "I encountered an error while processing your query. Please try again.",
265
+ "source": "Error",
266
+ "route": None,
267
+ "documents": None,
268
+ "provider": LLM_PROVIDER.upper(),
269
+ "error": str(e)
270
+ }
271
+
272
+ def check_system_health():
273
+ """Check if all components are properly initialized."""
274
+ try:
275
+ # Test ChromaDB connection
276
+ if vector_store:
277
+ vector_store._client.heartbeat()
278
+
279
+ # Test if all chains are initialized
280
+ components = {
281
+ "vector_store": vector_store is not None,
282
+ "company_qa_chain": company_qa_chain is not None,
283
+ "query_router_chain": query_router_chain is not None,
284
+ "cybersecurity_chain": cybersecurity_chain is not None,
285
+ "llm": llm is not None
286
+ }
287
+
288
+ return {
289
+ "status": "healthy" if all(components.values()) else "unhealthy",
290
+ "components": components,
291
+ "provider": LLM_PROVIDER.upper(),
292
+ "model": get_llm_config()["model"] if llm else "Not initialized"
293
+ }
294
+
295
+ except Exception as e:
296
+ return {
297
+ "status": "unhealthy",
298
+ "error": str(e),
299
+ "provider": LLM_PROVIDER.upper()
300
+ }
301
+
302
+ def test_llm_connection():
303
+ """Test the LLM API connection."""
304
+ try:
305
+ if not llm:
306
+ initialize_pipelines()
307
+
308
+ # Simple test query
309
+ test_response = llm("Say 'Hello, LLM is working!'")
310
+ return {
311
+ "success": True,
312
+ "provider": LLM_PROVIDER.upper(),
313
+ "model": get_llm_config()["model"],
314
+ "response": str(test_response)
315
+ }
316
+ except Exception as e:
317
+ return {
318
+ "success": False,
319
+ "provider": LLM_PROVIDER.upper(),
320
+ "error": str(e)
321
+ }
322
+
323
+ # Initialize pipelines on module import
324
+ try:
325
+ initialize_pipelines()
326
+ except Exception as e:
327
+ print(f"Failed to initialize pipelines on startup: {e}")
features/rag_chatbot/routes.py ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Request
2
+ from fastapi.security import HTTPBearer
3
+ from pydantic import BaseModel, Field
4
+ from slowapi.util import get_remote_address
5
+ from slowapi import Limiter
6
+ from typing import Optional
7
+ from config import ACCESS_RATE
8
+ from .controller import (
9
+ handle_rag_query,
10
+ handle_document_upload,
11
+ handle_health_check,
12
+ verify_token,
13
+ )
14
+
15
+ limiter = Limiter(key_func=get_remote_address)
16
+ router = APIRouter(prefix="/rag", tags=["RAG Chatbot"])
17
+ security = HTTPBearer()
18
+
19
+ class QueryInput(BaseModel):
20
+ query: str = Field(..., min_length=1, max_length=1000, description="The question to ask")
21
+
22
+ class QueryResponse(BaseModel):
23
+ answer: str
24
+ source: str
25
+ route: Optional[str] = None
26
+ documents: Optional[list] = None
27
+ error: Optional[str] = None
28
+
29
+ class UploadResponse(BaseModel):
30
+ message: str
31
+ filename: str
32
+ text_length: int
33
+ content_type: str
34
+
35
+ class HealthResponse(BaseModel):
36
+ status: str
37
+ components: Optional[dict] = None
38
+ error: Optional[str] = None
39
+
40
+ @router.post("/question", response_model=QueryResponse)
41
+ @limiter.limit(ACCESS_RATE)
42
+ async def ask_question(
43
+ request: Request,
44
+ data: QueryInput,
45
+ token: str = Depends(verify_token)
46
+ ) -> QueryResponse:
47
+ """
48
+ Ask a question to the RAG chatbot.
49
+
50
+ The chatbot can answer:
51
+ - Company-related questions (based on uploaded documents)
52
+ - Cybersecurity questions (from knowledge base)
53
+ """
54
+ response = await handle_rag_query(data.query)
55
+ return QueryResponse(**response)
56
+
57
+ @router.post("/upload", response_model=UploadResponse)
58
+ @limiter.limit(ACCESS_RATE)
59
+ async def upload_document(
60
+ request: Request,
61
+ file: UploadFile = File(..., description="Document file (PDF, DOCX, or TXT)"),
62
+ token: str = Depends(verify_token)
63
+ ) -> UploadResponse:
64
+ """
65
+ Upload a document to the company knowledge base.
66
+
67
+ Supported formats:
68
+ - PDF (.pdf)
69
+ - Word documents (.docx)
70
+ - Plain text (.txt)
71
+
72
+ Maximum file size: 10MB
73
+ """
74
+ response = await handle_document_upload(file)
75
+ return UploadResponse(**response)
76
+
77
+ @router.get("/health", response_model=HealthResponse)
78
+ @limiter.limit(ACCESS_RATE)
79
+ async def health_check(request: Request) -> HealthResponse:
80
+ """
81
+ Check the health status of the RAG system.
82
+
83
+ Returns the status of all components:
84
+ - ChromaDB connection
85
+ - Vector store
86
+ - AI chains
87
+ """
88
+ response = await handle_health_check()
89
+ return HealthResponse(**response)
90
+
91
+ @router.get("/info")
92
+ @limiter.limit(ACCESS_RATE)
93
+ async def get_system_info(request: Request):
94
+ """Get information about the RAG system capabilities."""
95
+ return {
96
+ "name": "RAG Chatbot",
97
+ "version": "1.0.0",
98
+ "description": "A specialized chatbot for cybersecurity and company-related questions",
99
+ "capabilities": [
100
+ "Company document Q&A (based on uploaded documents)",
101
+ "Cybersecurity knowledge and best practices",
102
+ "Document upload and processing (PDF, DOCX, TXT)"
103
+ ],
104
+ "supported_file_types": [
105
+ "application/pdf",
106
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
107
+ "text/plain"
108
+ ],
109
+ "max_file_size_mb": 10,
110
+ "max_query_length": 1000
111
+ }
requirements.txt CHANGED
@@ -18,3 +18,13 @@ scipy
18
  fitz
19
  frontend
20
  tools
 
 
 
 
 
 
 
 
 
 
 
18
  fitz
19
  frontend
20
  tools
21
+ langchain
22
+ langchain-community
23
+ langchain-openai
24
+ faiss-cpu
25
+ PyPDF2
26
+ tiktoken
27
+ chromadb
28
+ langchain_chroma
29
+ sentence-transformers
30
+ tf-keras