diff --git a/Domains/AI-ML/MiniProjects/academic_chatbot/.gitignore b/Domains/AI-ML/MiniProjects/academic_chatbot/.gitignore new file mode 100644 index 00000000..c66ba3a1 --- /dev/null +++ b/Domains/AI-ML/MiniProjects/academic_chatbot/.gitignore @@ -0,0 +1,2 @@ +.env +.venv \ No newline at end of file diff --git a/Domains/AI-ML/MiniProjects/academic_chatbot/README.md b/Domains/AI-ML/MiniProjects/academic_chatbot/README.md new file mode 100755 index 00000000..99cd372d --- /dev/null +++ b/Domains/AI-ML/MiniProjects/academic_chatbot/README.md @@ -0,0 +1,36 @@ + +**Contributor:** Mmadan128 + +# Academic Chatbot (LangChain + Gemini) + +## Quickstart + +1. Create and activate a virtual environment. +2. Install deps: +``` +pip install -r requirements.txt +``` +3. Set environment: +``` +copy .env.example .env # Windows +# Set GOOGLE_API_KEY in .env +``` +4. Ingest sample data (place PDFs/text in `data/`): +``` +python -m app.ingest_cli example.pdf +``` +5. Run API: +``` +uvicorn app.server:app --reload --host 0.0.0.0 --port 8000 +``` +6. Open `public/widget.html` and set `window.CAMPUS_ASSISTANT_API` if needed. + +## Environment +- GOOGLE_API_KEY: Google Generative AI key +- EMBEDDING_MODEL: sentence-transformers multilingual model + +## Ingestion +Use `/ingest` or the CLI to add PDFs/TXT. The FAISS store is persisted in `vectorstore/`. + +## Privacy +No chat content is stored beyond daily JSONL logs; configure retention as needed. diff --git a/Domains/AI-ML/MiniProjects/academic_chatbot/app/__init__.py b/Domains/AI-ML/MiniProjects/academic_chatbot/app/__init__.py new file mode 100755 index 00000000..3ba0f1a0 --- /dev/null +++ b/Domains/AI-ML/MiniProjects/academic_chatbot/app/__init__.py @@ -0,0 +1 @@ +__all__ = [] diff --git a/Domains/AI-ML/MiniProjects/academic_chatbot/app/__pycache__/__init__.cpython-312.pyc b/Domains/AI-ML/MiniProjects/academic_chatbot/app/__pycache__/__init__.cpython-312.pyc new file mode 100755 index 00000000..121e7ece Binary files /dev/null and b/Domains/AI-ML/MiniProjects/academic_chatbot/app/__pycache__/__init__.cpython-312.pyc differ diff --git a/Domains/AI-ML/MiniProjects/academic_chatbot/app/__pycache__/config.cpython-312.pyc b/Domains/AI-ML/MiniProjects/academic_chatbot/app/__pycache__/config.cpython-312.pyc new file mode 100755 index 00000000..49b19516 Binary files /dev/null and b/Domains/AI-ML/MiniProjects/academic_chatbot/app/__pycache__/config.cpython-312.pyc differ diff --git a/Domains/AI-ML/MiniProjects/academic_chatbot/app/__pycache__/ingest.cpython-312.pyc b/Domains/AI-ML/MiniProjects/academic_chatbot/app/__pycache__/ingest.cpython-312.pyc new file mode 100755 index 00000000..06f5175b Binary files /dev/null and b/Domains/AI-ML/MiniProjects/academic_chatbot/app/__pycache__/ingest.cpython-312.pyc differ diff --git a/Domains/AI-ML/MiniProjects/academic_chatbot/app/__pycache__/llm.cpython-312.pyc b/Domains/AI-ML/MiniProjects/academic_chatbot/app/__pycache__/llm.cpython-312.pyc new file mode 100755 index 00000000..23279705 Binary files /dev/null and b/Domains/AI-ML/MiniProjects/academic_chatbot/app/__pycache__/llm.cpython-312.pyc differ diff --git a/Domains/AI-ML/MiniProjects/academic_chatbot/app/__pycache__/logging_utils.cpython-312.pyc b/Domains/AI-ML/MiniProjects/academic_chatbot/app/__pycache__/logging_utils.cpython-312.pyc new file mode 100755 index 00000000..e4223d3d Binary files /dev/null and b/Domains/AI-ML/MiniProjects/academic_chatbot/app/__pycache__/logging_utils.cpython-312.pyc differ diff --git a/Domains/AI-ML/MiniProjects/academic_chatbot/app/__pycache__/schemas.cpython-312.pyc b/Domains/AI-ML/MiniProjects/academic_chatbot/app/__pycache__/schemas.cpython-312.pyc new file mode 100755 index 00000000..87a54310 Binary files /dev/null and b/Domains/AI-ML/MiniProjects/academic_chatbot/app/__pycache__/schemas.cpython-312.pyc differ diff --git a/Domains/AI-ML/MiniProjects/academic_chatbot/app/__pycache__/server.cpython-312.pyc b/Domains/AI-ML/MiniProjects/academic_chatbot/app/__pycache__/server.cpython-312.pyc new file mode 100755 index 00000000..dde5dd9e Binary files /dev/null and b/Domains/AI-ML/MiniProjects/academic_chatbot/app/__pycache__/server.cpython-312.pyc differ diff --git a/Domains/AI-ML/MiniProjects/academic_chatbot/app/__pycache__/vectordb.cpython-312.pyc b/Domains/AI-ML/MiniProjects/academic_chatbot/app/__pycache__/vectordb.cpython-312.pyc new file mode 100755 index 00000000..e6e43943 Binary files /dev/null and b/Domains/AI-ML/MiniProjects/academic_chatbot/app/__pycache__/vectordb.cpython-312.pyc differ diff --git a/Domains/AI-ML/MiniProjects/academic_chatbot/app/config.py b/Domains/AI-ML/MiniProjects/academic_chatbot/app/config.py new file mode 100755 index 00000000..0e43efb2 --- /dev/null +++ b/Domains/AI-ML/MiniProjects/academic_chatbot/app/config.py @@ -0,0 +1,31 @@ +import os +from pathlib import Path +from dotenv import load_dotenv + +load_dotenv(override=True) + +BASE_DIR = Path(__file__).resolve().parent.parent +DATA_DIR = BASE_DIR / "data" +VECTOR_DIR = BASE_DIR / "vectorstore" +LOGS_DIR = BASE_DIR / "logs" +PUBLIC_DIR = BASE_DIR / "public" + +GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY", "") +CONTACT_FALLBACK = os.getenv("CONTACT_FALLBACK", "contact@college.example") + +DEFAULT_EMBEDDING_MODEL = os.getenv( + "EMBEDDING_MODEL", + "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2", +) + +GEN_AI_MODEL = os.getenv("GEN_AI_MODEL", "gemini-2.0-flash") +SIMILARITY_FALLBACK_THRESHOLD = float(os.getenv("SIMILARITY_FALLBACK_THRESHOLD", "0.35")) +TOP_K = int(os.getenv("TOP_K", "5")) + +# OCR settings +OCR_LANGS = os.getenv("OCR_LANGS", "eng+hin+ben+mar+tam").split("+") +TESSERACT_CMD = os.getenv("TESSERACT_CMD", "") # Optional: full path to tesseract.exe +POPPLER_PATH = os.getenv("POPPLER_PATH", "") # Optional: path to poppler bin (for pdf2image on Windows) + +for d in (DATA_DIR, VECTOR_DIR, LOGS_DIR, PUBLIC_DIR): + d.mkdir(parents=True, exist_ok=True) diff --git a/Domains/AI-ML/MiniProjects/academic_chatbot/app/ingest.py b/Domains/AI-ML/MiniProjects/academic_chatbot/app/ingest.py new file mode 100755 index 00000000..f84b68a7 --- /dev/null +++ b/Domains/AI-ML/MiniProjects/academic_chatbot/app/ingest.py @@ -0,0 +1,234 @@ +from pathlib import Path +from typing import Iterable, List, Optional, Tuple + +from langchain.docstore.document import Document +from langchain.text_splitter import RecursiveCharacterTextSplitter +from langchain_community.document_loaders import PyPDFLoader, TextLoader +import pandas as pd +import re + +# OCR imports +try: + from pdf2image import convert_from_path + import pytesseract +except Exception: + convert_from_path = None # type: ignore + pytesseract = None # type: ignore + +from .config import DATA_DIR, OCR_LANGS, TESSERACT_CMD, POPPLER_PATH +from .vectordb import add_documents + + +CHUNK_SIZE = 800 +CHUNK_OVERLAP = 120 + + +def _split_documents(documents: List[Document]) -> List[Document]: + splitter = RecursiveCharacterTextSplitter( + chunk_size=CHUNK_SIZE, + chunk_overlap=CHUNK_OVERLAP, + separators=["\n\n", "\n", " ", ""], + ) + return splitter.split_documents(documents) + + +def _collect_paths(inputs: List[Path]) -> List[Path]: + collected: List[Path] = [] + allowed_ext = {".pdf", ".txt", ".md", ".csv", ".xlsx", ".xls"} + for p in inputs: + if p.is_dir(): + for fp in p.rglob("*"): + if fp.is_file() and fp.suffix.lower() in allowed_ext: + collected.append(fp) + elif p.is_file() and p.suffix.lower() in allowed_ext: + collected.append(p) + return collected + + +def _normalize_col(name: str) -> str: + name = str(name or "").strip().lower() + name = re.sub(r"[_\s]+", " ", name) + return name + + +def _row_to_text(df: pd.DataFrame, row: pd.Series, extra_keywords: str = "") -> str: + parts: List[str] = [] + for col in df.columns: + val = row.get(col, "") + if pd.isna(val) or str(val).strip() == "": + continue + text_val = str(val).strip() + parts.append(f"{col}: {text_val}") + if extra_keywords: + parts.append(f"keywords: {extra_keywords}") + return "\n".join(parts) + + +def _detect_header_index(df_no_header: pd.DataFrame) -> int: + # Heuristic: first row with >=50% non-empty cells is the header + threshold = max(1, int(0.5 * df_no_header.shape[1])) + for idx in range(len(df_no_header)): + non_empty = (df_no_header.iloc[idx].astype(str).str.strip() != "").sum() + if non_empty >= threshold: + return idx + return 0 + + +def _read_excel_with_header(path: Path, sheet_name: str) -> pd.DataFrame: + # Read without header, detect header row, then reassign columns + df0 = pd.read_excel(path, sheet_name=sheet_name, header=None, dtype=str, keep_default_na=False) + if df0.empty: + return df0 + header_idx = _detect_header_index(df0) + header = df0.iloc[header_idx].astype(str).tolist() + df = df0.iloc[header_idx + 1 :].copy() + df.columns = [_normalize_col(h) for h in header] + # Drop fully empty rows + df = df.replace({None: ""}) + df = df[~df.isna().all(axis=1)] + return df + + +def _sheet_keywords(sheet: str, df: pd.DataFrame) -> str: + kw = [sheet] + # If there is a department-like column, include its unique values + for cand in ["dept", "department", "branch", "programme", "program"]: + if cand in df.columns: + vals = sorted({str(v).strip() for v in df[cand].tolist() if str(v).strip()}) + kw.extend(vals) + # Add common synonyms to aid retrieval + kw.extend(["faculty", "staff", "professor", "teacher", "hod", "contact"]) + return ", ".join(sorted({k.lower() for k in kw if k})) + + +def _load_tabular(path: Path) -> List[Document]: + docs: List[Document] = [] + if path.suffix.lower() == ".csv": + df = pd.read_csv(path, dtype=str, keep_default_na=False, na_filter=False) + if not df.empty: + # Normalize columns + df.columns = [_normalize_col(c) for c in df.columns] + df = df.dropna(how="all") + sheet_kw = _sheet_keywords(path.stem, df) + for idx, row in df.iterrows(): + text = _row_to_text(df, row, extra_keywords=sheet_kw) + if not text.strip(): + continue + docs.append( + Document( + page_content=text, + metadata={"source": str(path), "type": "csv", "row": int(idx)}, + ) + ) + # Add a sheet-level summary to help queries like "CSE faculty" + preview_cols = df.columns[:6] + lines = [ + f"Row {i}: " + + ", ".join(f"{c}={str(df.iloc[i][c]).strip()}" for c in preview_cols if str(df.iloc[i][c]).strip()) + for i in range(min(25, len(df))) + ] + if lines: + docs.append( + Document( + page_content=f"Summary of {path.name}:\n" + "\n".join(lines) + f"\nkeywords: {sheet_kw}", + metadata={"source": str(path), "type": "csv_summary"}, + ) + ) + else: + xls = pd.ExcelFile(path) + for sheet in xls.sheet_names: + df = _read_excel_with_header(path, sheet) + if df.empty: + continue + sheet_kw = _sheet_keywords(sheet, df) + for idx, row in df.iterrows(): + text = _row_to_text(df, row, extra_keywords=sheet_kw) + if not text.strip(): + continue + docs.append( + Document( + page_content=text, + metadata={ + "source": str(path), + "type": "excel", + "sheet": sheet, + "row": int(idx), + }, + ) + ) + # Sheet-level summary + preview_cols = df.columns[:6] + lines = [ + f"Row {i}: " + + ", ".join(f"{c}={str(df.iloc[i][c]).strip()}" for c in preview_cols if str(df.iloc[i][c]).strip()) + for i in range(min(25, len(df))) + ] + if lines: + docs.append( + Document( + page_content=f"Summary of sheet {sheet} in {path.name}:\n" + + "\n".join(lines) + + f"\nkeywords: {sheet_kw}", + metadata={ + "source": str(path), + "type": "excel_summary", + "sheet": sheet, + }, + ) + ) + return docs + + +def _pdf_ocr_fallback(path: Path) -> List[Document]: + if convert_from_path is None or pytesseract is None: + return [] + if TESSERACT_CMD: + pytesseract.pytesseract.tesseract_cmd = TESSERACT_CMD + images = convert_from_path(str(path), dpi=300, poppler_path=POPPLER_PATH or None) + docs: List[Document] = [] + langs = "+".join(OCR_LANGS) if OCR_LANGS else "eng" + for i, img in enumerate(images): + text = pytesseract.image_to_string(img, lang=langs) + if text and text.strip(): + docs.append( + Document( + page_content=text, + metadata={"source": str(path), "type": "pdf_ocr", "page": i + 1}, + ) + ) + return docs + + +def load_files(paths: List[Path]) -> List[Document]: + docs: List[Document] = [] + for p in paths: + suf = p.suffix.lower() + if suf == ".pdf": + try: + loader = PyPDFLoader(str(p)) + loaded = loader.load() + if not loaded or not any((d.page_content or "").strip() for d in loaded): + ocr_docs = _pdf_ocr_fallback(p) + docs.extend(ocr_docs) + else: + docs.extend(loaded) + except Exception: + ocr_docs = _pdf_ocr_fallback(p) + docs.extend(ocr_docs) + elif suf in {".txt", ".md"}: + loader = TextLoader(str(p), encoding="utf-8") + docs.extend(loader.load()) + elif suf in {".csv", ".xlsx", ".xls"}: + docs.extend(_load_tabular(p)) + return docs + + +def ingest_local(paths: List[str]) -> int: + if not paths: + base_inputs = [DATA_DIR] + else: + base_inputs = [Path(p) if Path(p).is_absolute() else Path(DATA_DIR) / p for p in paths] + file_paths = _collect_paths(base_inputs) + raw_docs = load_files(file_paths) + chunks = _split_documents(raw_docs) + return add_documents(chunks) diff --git a/Domains/AI-ML/MiniProjects/academic_chatbot/app/ingest_cli.py b/Domains/AI-ML/MiniProjects/academic_chatbot/app/ingest_cli.py new file mode 100755 index 00000000..03e8b53c --- /dev/null +++ b/Domains/AI-ML/MiniProjects/academic_chatbot/app/ingest_cli.py @@ -0,0 +1,15 @@ +import sys +from pathlib import Path +from .ingest import ingest_local + + +def main(): + if len(sys.argv) < 2: + print("Usage: python -m app.ingest_cli [file2 ...]") + sys.exit(1) + added = ingest_local(sys.argv[1:]) + print(f"Added {added} chunks") + + +if __name__ == "__main__": + main() diff --git a/Domains/AI-ML/MiniProjects/academic_chatbot/app/llm.py b/Domains/AI-ML/MiniProjects/academic_chatbot/app/llm.py new file mode 100755 index 00000000..780cf42d --- /dev/null +++ b/Domains/AI-ML/MiniProjects/academic_chatbot/app/llm.py @@ -0,0 +1,27 @@ +from typing import List, Tuple +from langchain_core.prompts import ChatPromptTemplate +from langchain_google_genai import ChatGoogleGenerativeAI +from langchain_core.output_parsers import StrOutputParser + +from .config import GOOGLE_API_KEY, GEN_AI_MODEL + + +def build_llm(): + if not GOOGLE_API_KEY: + raise RuntimeError("GOOGLE_API_KEY not set") + return ChatGoogleGenerativeAI(model=GEN_AI_MODEL, google_api_key=GOOGLE_API_KEY, temperature=0.2) + + +SYSTEM_PROMPT = ( + "You are a helpful, concise campus assistant. Answer in the user's language. " + "Cite from provided context only. If unsure, say you are not certain and suggest the official contact." +) + + +def build_rag_chain(retriever): + prompt = ChatPromptTemplate.from_messages([ + ("system", SYSTEM_PROMPT + "\nContext:\n{context}"), + ("human", "{question}"), + ]) + llm = build_llm() + return prompt | llm | StrOutputParser() diff --git a/Domains/AI-ML/MiniProjects/academic_chatbot/app/logging_utils.py b/Domains/AI-ML/MiniProjects/academic_chatbot/app/logging_utils.py new file mode 100755 index 00000000..6eb86606 --- /dev/null +++ b/Domains/AI-ML/MiniProjects/academic_chatbot/app/logging_utils.py @@ -0,0 +1,16 @@ +from __future__ import annotations +import orjson +from datetime import datetime +from pathlib import Path +from typing import Any, Dict + +from .config import LOGS_DIR + + +def log_event(event: Dict[str, Any]) -> None: + ts = datetime.utcnow() + fn = LOGS_DIR / f"conversations_{ts.strftime('%Y-%m-%d')}.jsonl" + record = {"ts": ts.isoformat() + "Z", **event} + with open(fn, "ab") as f: + f.write(orjson.dumps(record)) + f.write(b"\n") diff --git a/Domains/AI-ML/MiniProjects/academic_chatbot/app/schemas.py b/Domains/AI-ML/MiniProjects/academic_chatbot/app/schemas.py new file mode 100755 index 00000000..345b7354 --- /dev/null +++ b/Domains/AI-ML/MiniProjects/academic_chatbot/app/schemas.py @@ -0,0 +1,18 @@ +from pydantic import BaseModel, Field +from typing import List, Optional + + +class IngestRequest(BaseModel): + files: List[str] = Field(default_factory=list, description="List of filenames under data/") + + +class ChatRequest(BaseModel): + session_id: str + message: str + language: str = "auto" + + +class ChatResponse(BaseModel): + answer: str + fallback_used: bool = False + sources: List[str] = Field(default_factory=list) diff --git a/Domains/AI-ML/MiniProjects/academic_chatbot/app/server.py b/Domains/AI-ML/MiniProjects/academic_chatbot/app/server.py new file mode 100755 index 00000000..896561c9 --- /dev/null +++ b/Domains/AI-ML/MiniProjects/academic_chatbot/app/server.py @@ -0,0 +1,78 @@ +from typing import Dict, List + +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from langchain.docstore.document import Document + +from .schemas import IngestRequest, ChatRequest, ChatResponse +from .ingest import ingest_local +from .vectordb import similarity_search +from .llm import build_rag_chain +from .config import TOP_K, SIMILARITY_FALLBACK_THRESHOLD, CONTACT_FALLBACK +from .logging_utils import log_event + +app = FastAPI(title="Campus Assistant", version="0.1.0") +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +_sessions: Dict[str, List[Dict[str, str]]] = {} + + +@app.get("/health") +async def health(): + return {"ok": True} + + +@app.post("/ingest") +async def ingest(req: IngestRequest): + added = ingest_local(req.files) + return {"added_chunks": added} + + +@app.post("/chat", response_model=ChatResponse) +async def chat(req: ChatRequest): + history = _sessions.setdefault(req.session_id, []) + + docs = similarity_search(req.message, k=TOP_K) + context_text = "\n\n".join(d.page_content for d in docs) + sources = list({str(d.metadata.get("source", "unknown")) for d in docs}) + + chain = build_rag_chain(retriever=None) + answer = await chain.ainvoke({"context": context_text, "question": req.message}) + + max_score = 0.0 + if hasattr(docs, "scores"): + max_score = max((s or 0.0) for s in getattr(docs, "scores")) + # Heuristic: if no context or low similarity, append fallback + fallback_used = False + if not docs or len(context_text.strip()) == 0: + fallback_used = True + if fallback_used or max_score < SIMILARITY_FALLBACK_THRESHOLD: + fallback_used = True + answer = ( + answer.strip() + + f"\n\nIf this doesn't answer your question, please contact: {CONTACT_FALLBACK}" + ) + + history.append({"user": req.message, "assistant": answer}) + if len(history) > 20: + _sessions[req.session_id] = history[-20:] + + log_event( + { + "type": "chat", + "session_id": req.session_id, + "message": req.message, + "answer": answer, + "sources": sources, + "fallback": fallback_used, + } + ) + + return ChatResponse(answer=answer, fallback_used=fallback_used, sources=sources) diff --git a/Domains/AI-ML/MiniProjects/academic_chatbot/app/vectordb.py b/Domains/AI-ML/MiniProjects/academic_chatbot/app/vectordb.py new file mode 100755 index 00000000..43cfa51b --- /dev/null +++ b/Domains/AI-ML/MiniProjects/academic_chatbot/app/vectordb.py @@ -0,0 +1,44 @@ +from pathlib import Path +from typing import List, Optional + +from langchain_community.vectorstores import FAISS +from langchain_community.embeddings import HuggingFaceEmbeddings +from langchain.docstore.document import Document + +from .config import VECTOR_DIR, DEFAULT_EMBEDDING_MODEL + + +_embedding = None + + +def get_embeddings(): + global _embedding + if _embedding is None: + _embedding = HuggingFaceEmbeddings(model_name=DEFAULT_EMBEDDING_MODEL) + return _embedding + + +def get_vectorstore(name: str = "campus_faqs") -> FAISS: + path = Path(VECTOR_DIR) / f"{name}.faiss" + embeddings = get_embeddings() + if path.exists(): + return FAISS.load_local(str(path), embeddings, allow_dangerous_deserialization=True) + vs = FAISS.from_texts([""], embedding=embeddings) + vs.save_local(str(path)) + return vs + + +def add_documents(documents: List[Document], name: str = "campus_faqs") -> int: + vs = get_vectorstore(name) + embeddings = get_embeddings() + if documents: + new_vs = FAISS.from_documents(documents, embeddings) + vs.merge_from(new_vs) + vs.save_local(str(Path(VECTOR_DIR) / f"{name}.faiss")) + return len(documents) + return 0 + + +def similarity_search(query: str, k: int = 5, name: str = "campus_faqs") -> List[Document]: + vs = get_vectorstore(name) + return vs.similarity_search(query, k=k) diff --git a/Domains/AI-ML/MiniProjects/academic_chatbot/data/excels/CSE_Faculty.xlsx b/Domains/AI-ML/MiniProjects/academic_chatbot/data/excels/CSE_Faculty.xlsx new file mode 100755 index 00000000..c6f7f7b7 Binary files /dev/null and b/Domains/AI-ML/MiniProjects/academic_chatbot/data/excels/CSE_Faculty.xlsx differ diff --git a/Domains/AI-ML/MiniProjects/academic_chatbot/data/excels/Facillities.xlsx b/Domains/AI-ML/MiniProjects/academic_chatbot/data/excels/Facillities.xlsx new file mode 100755 index 00000000..52ef9afb Binary files /dev/null and b/Domains/AI-ML/MiniProjects/academic_chatbot/data/excels/Facillities.xlsx differ diff --git a/Domains/AI-ML/MiniProjects/academic_chatbot/data/excels/MAIT_Administrative_Procedures.xlsx b/Domains/AI-ML/MiniProjects/academic_chatbot/data/excels/MAIT_Administrative_Procedures.xlsx new file mode 100755 index 00000000..3771c0e6 Binary files /dev/null and b/Domains/AI-ML/MiniProjects/academic_chatbot/data/excels/MAIT_Administrative_Procedures.xlsx differ diff --git a/Domains/AI-ML/MiniProjects/academic_chatbot/data/excels/MAIT_Societies.xlsx b/Domains/AI-ML/MiniProjects/academic_chatbot/data/excels/MAIT_Societies.xlsx new file mode 100755 index 00000000..35ad0dfc Binary files /dev/null and b/Domains/AI-ML/MiniProjects/academic_chatbot/data/excels/MAIT_Societies.xlsx differ diff --git a/Domains/AI-ML/MiniProjects/academic_chatbot/data/excels/academic_calendar.xlsx b/Domains/AI-ML/MiniProjects/academic_chatbot/data/excels/academic_calendar.xlsx new file mode 100755 index 00000000..e07efeba Binary files /dev/null and b/Domains/AI-ML/MiniProjects/academic_chatbot/data/excels/academic_calendar.xlsx differ diff --git a/Domains/AI-ML/MiniProjects/academic_chatbot/data/excels/syllabus.csv b/Domains/AI-ML/MiniProjects/academic_chatbot/data/excels/syllabus.csv new file mode 100755 index 00000000..d97007a9 --- /dev/null +++ b/Domains/AI-ML/MiniProjects/academic_chatbot/data/excels/syllabus.csv @@ -0,0 +1,46 @@ +Category,Group,Code,Paper,L,P,Credits,Notes +First Semester,,,,,,, +Theory Papers,,,,,,, +,ES,ES-101,Programming in 'C',3,-,3,"*Any one of the following" +,BS,"BS-103/BS-121#",Applied Chemistry / Basic Chemistry#,3,-,3,"*Any one of the following" +,BS,BS-105,Applied Physics – I,3,-,3, +,ES,ES-107,Electrical Science,3,-,3,"*Any one of the following" +,BS,BS-109,Environmental Studies,3,-,3,"*Any one of the following" +,BS,BS-111,Applied Mathematics – I,4,-,4, +,HS,HS-113,"Group 1: Communications Skills",3,-,3,"**Group 1 or Group 2 shall be offered" +,HS,HS-113,"OR Group 2: Indian Constitution***",2,-,2, +,HS,HS-117,Human Values and Ethics***,1,-,1, +,ES,ES-119,Manufacturing Process,4,-,4, +Practical/Viva Voce,,,,,,, +,BS,BS-151,Physics-I Lab,-,2,1, +,ES,ES-153,Programming in ‘C’ Lab,-,2,1,"Any of the following corresponding to the theory paper offered" +,BS,BS-155,Applied Chemistry Lab,-,2,1,"Any of the following corresponding to the theory paper offered" +,ES,ES-157,Engineering Graphics-I,-,4,2,"Any of the following corresponding to the theory paper offered" +,ES,ES-159,Electrical Science Lab,-,2,1,"Any of the following corresponding to the theory paper offered" +,BS,BS-161,Environmental Studies Lab,-,2,1,"Any of the following corresponding to the theory paper offered" +,,,,,,, +Total,,,,20,10,25, + +Category,Group,Paper Code,Paper,L,P,Credits,Notes +Second Semester,,,,,,, +Theory Papers,,,,,,, +,ES,ES-102,"Programming in 'C'",3,-,3,"*Any one of the following" +,BS,"BS-104/BS-120#","Applied Chemistry / Basic Chemistry#",3,-,3,"*Any one of the following" +,BS,BS-106,Applied Physics – II,3,-,3, +,ES,ES-108,Electrical Science,3,-,3,"*Any one of the following" +,BS,BS-110,Environmental Studies,3,-,3,"*Any one of the following" +,BS,BS-112,Applied Mathematics – II,4,-,4, +,HS,HS-114,"Group 1: Communications Skills",3,-,3,"**Group 1 or Group 2 shall be offered" +,HS,HS-114,"OR Group 2: Indian Constitution***",2,-,2,"**Group 1 or Group 2 shall be offered" +,HS,HS-118,Human Values and Ethics***,1,-,1, +,ES,ES-114,Engineering Mechanics,3,-,3, +Practical/Viva Voce,,,,,,, +,BS,BS-152,Physics-II Lab,-,2,1, +,ES,ES-154,"Programming in 'C' Lab",-,2,1,"*Any of the following corresponding to the theory paper offered" +,BS,BS-156,Applied Chemistry,-,2,1,"*Any of the following corresponding to the theory paper offered" +,ES,ES-158,Engineering Graphics-II,-,2,1, +,ES,ES-160,Electrical Science Lab,-,2,1,"*Any of the following corresponding to the theory paper offered" +,BS,BS-162,Environmental Studies Lab,-,2,1,"*Any of the following corresponding to the theory paper offered" +,ES,ES-164,Workshop Practice,-,4,2, +,,,,,,, +Total,,,,19,12,25, \ No newline at end of file diff --git a/Domains/AI-ML/MiniProjects/academic_chatbot/data/excels/syllabus.txt b/Domains/AI-ML/MiniProjects/academic_chatbot/data/excels/syllabus.txt new file mode 100755 index 00000000..e69de29b diff --git a/Domains/AI-ML/MiniProjects/academic_chatbot/data/excels/syllabus.xlsx b/Domains/AI-ML/MiniProjects/academic_chatbot/data/excels/syllabus.xlsx new file mode 100755 index 00000000..b987019a Binary files /dev/null and b/Domains/AI-ML/MiniProjects/academic_chatbot/data/excels/syllabus.xlsx differ diff --git a/Domains/AI-ML/MiniProjects/academic_chatbot/data/pdfs/sem1_syllabus.pdf b/Domains/AI-ML/MiniProjects/academic_chatbot/data/pdfs/sem1_syllabus.pdf new file mode 100755 index 00000000..3a50e813 Binary files /dev/null and b/Domains/AI-ML/MiniProjects/academic_chatbot/data/pdfs/sem1_syllabus.pdf differ diff --git a/Domains/AI-ML/MiniProjects/academic_chatbot/main.py b/Domains/AI-ML/MiniProjects/academic_chatbot/main.py new file mode 100755 index 00000000..d2fd166a --- /dev/null +++ b/Domains/AI-ML/MiniProjects/academic_chatbot/main.py @@ -0,0 +1,4 @@ +import uvicorn + +if __name__ == "__main__": + uvicorn.run("app.server:app", host="0.0.0.0", port=8000) diff --git a/Domains/AI-ML/MiniProjects/academic_chatbot/public/widget.html b/Domains/AI-ML/MiniProjects/academic_chatbot/public/widget.html new file mode 100755 index 00000000..5a532034 --- /dev/null +++ b/Domains/AI-ML/MiniProjects/academic_chatbot/public/widget.html @@ -0,0 +1,45 @@ + + + + + + Campus Assistant Widget + + + +
+ +
+
+ + +
+
+ + + diff --git a/Domains/AI-ML/MiniProjects/academic_chatbot/requirements.txt b/Domains/AI-ML/MiniProjects/academic_chatbot/requirements.txt new file mode 100755 index 00000000..099d44d2 --- /dev/null +++ b/Domains/AI-ML/MiniProjects/academic_chatbot/requirements.txt @@ -0,0 +1,23 @@ +fastapi==0.115.0 +uvicorn[standard]==0.30.6 +pydantic==2.9.2 +python-dotenv==1.0.1 +langchain==0.3.3 +langchain-community==0.3.2 +langchain-google-genai==2.0.1 +google-generativeai==0.8.3 +sentence-transformers==3.1.1 +faiss-cpu==1.8.0.post1 +pypdf==5.0.1 +python-multipart==0.0.12 +tqdm==4.66.5 +orjson==3.10.7 +pandas==2.2.3 +openpyxl==3.1.5 +streamlit==1.40.1 +streamlit-mic-recorder==0.0.8 +SpeechRecognition==3.11.0 +gTTS==2.5.4 +pydub==0.25.1 +pdf2image==1.17.0 +pytesseract==0.3.10 diff --git a/Domains/AI-ML/MiniProjects/academic_chatbot/stream_app.py b/Domains/AI-ML/MiniProjects/academic_chatbot/stream_app.py new file mode 100755 index 00000000..2726ed85 --- /dev/null +++ b/Domains/AI-ML/MiniProjects/academic_chatbot/stream_app.py @@ -0,0 +1,157 @@ +import os +import io +import base64 +import streamlit as st +from typing import List + +from app.ingest import ingest_local +from app.vectordb import similarity_search +from app.llm import build_rag_chain +from app.config import TOP_K, CONTACT_FALLBACK, GOOGLE_API_KEY + +# Voice deps +try: + from streamlit_mic_recorder import mic_recorder + import speech_recognition as sr + from gtts import gTTS + from pydub import AudioSegment +except Exception: + mic_recorder = None + +st.set_page_config(page_title="Campus Assistant - Streamlit (Local)", page_icon="🎓", layout="centered") + +st.title("🎓 Campus Assistant - Local Prototype") + +if not GOOGLE_API_KEY: + st.error("GOOGLE_API_KEY not set. Add it to your environment or .env and restart.") + +if "messages" not in st.session_state: + st.session_state["messages"] = [] +if "session_id" not in st.session_state: + st.session_state["session_id"] = os.urandom(6).hex() + +# Language selection (UI language and TTS language) +LANG_OPTIONS = { + "English (en)": "en", + "Hindi (hi)": "hi", + "Bengali (bn)": "bn", + "Marathi (mr)": "mr", + "Tamil (ta)": "ta", + "Telugu (te)": "te", + "Kannada (kn)": "kn", + "Gujarati (gu)": "gu", +} +col_lang, col_tts = st.columns(2) +with col_lang: + ui_lang = st.selectbox("Preferred language", list(LANG_OPTIONS.keys()), index=0) +with col_tts: + tts_lang = st.selectbox("Voice reply language", list(LANG_OPTIONS.keys()), index=0) +ui_lang_code = LANG_OPTIONS[ui_lang] +tts_lang_code = LANG_OPTIONS[tts_lang] + +with st.sidebar: + st.header("Ingestion (Local)") + st.caption("Reads from the data/ directory recursively: pdfs, txt, md, csv, xlsx, xls") + if st.button("Ingest: All under data/"): + try: + added = ingest_local([]) + st.success(f"Added {added} chunks") + except Exception as e: + st.error(str(e)) + subfolders = st.text_input("Or subfolders (comma-separated)", value="pdfs, excels") + if st.button("Ingest: Selected subfolders"): + items: List[str] = [s.strip() for s in subfolders.split(",") if s.strip()] + try: + added = ingest_local(items) + st.success(f"Added {added} chunks from: {items}") + except Exception as e: + st.error(str(e)) + +st.write("") +st.caption(f"Session: {st.session_state['session_id']}") + +# Chat history +for msg in st.session_state["messages"]: + if msg["role"] in ("user", "assistant"): + st.chat_message(msg["role"]).write(msg["content"]) + if msg["role"] == "assistant" and msg.get("audio_b64"): + st.audio(msg["audio_b64"], format="audio/mp3") + + +def _transcode_to_wav_bytes(raw_bytes: bytes) -> bytes: + # Try to decode with pydub using common browser formats, export WAV mono 16k + last_error = None + for fmt in (None, "webm", "ogg", "mp3", "wav"): + try: + seg = AudioSegment.from_file(io.BytesIO(raw_bytes), format=fmt) if fmt else AudioSegment.from_file(io.BytesIO(raw_bytes)) + seg = seg.set_channels(1).set_frame_rate(16000) + out = io.BytesIO() + seg.export(out, format="wav") + out.seek(0) + return out.read() + except Exception as e: + last_error = e + continue + raise RuntimeError(f"Could not transcode audio to WAV. Install ffmpeg and retry. Details: {last_error}") + +# Input row: text + optional mic +col_text, col_mic = st.columns([3,1]) +with col_text: + prompt = st.chat_input("Ask about fees, scholarships, timetable, etc.") +voice_prompt = None +with col_mic: + if mic_recorder is not None: + audio = mic_recorder(start_prompt="🎙️ Voice", stop_prompt="Stop", just_once=True, use_container_width=True) + if audio and audio.get("bytes"): + try: + wav_bytes = _transcode_to_wav_bytes(audio["bytes"]) if AudioSegment else audio["bytes"] + rec = sr.Recognizer() + with sr.AudioFile(io.BytesIO(wav_bytes)) as source: + audio_data = rec.record(source) + voice_prompt = rec.recognize_google(audio_data, language=ui_lang_code) + except Exception as e: + st.warning(f"Voice recognition error: {e}\nTip: install ffmpeg and ensure it's on PATH.") + +user_input = prompt or voice_prompt + +if user_input: + st.session_state["messages"].append({"role": "user", "content": user_input}) + st.chat_message("user").write(user_input) + + try: + docs = similarity_search(user_input, k=TOP_K) + context_text = "\n\n".join(d.page_content for d in docs) + sources = list({str(d.metadata.get("source", "unknown")) for d in docs}) + + chain = build_rag_chain(retriever=None) + answer = chain.invoke({"context": context_text, "question": user_input}) + if not docs or not context_text.strip(): + answer = answer.strip() + f"\n\nIf this doesn't answer your question, please contact: {CONTACT_FALLBACK}" + + entry = {"role": "assistant", "content": answer} + + # TTS synthesis + try: + tts = gTTS(text=answer, lang=tts_lang_code) + buf = io.BytesIO() + tts.write_to_fp(buf) + buf.seek(0) + audio_b64 = "data:audio/mp3;base64," + base64.b64encode(buf.read()).decode("ascii") + entry["audio_b64"] = audio_b64 + except Exception: + pass + + st.session_state["messages"].append(entry) + st.chat_message("assistant").write(answer) + if entry.get("audio_b64"): + st.audio(entry["audio_b64"], format="audio/mp3") + + if sources: + with st.expander("Sources"): + for s in sources: + st.write(f"- {s}") + + except Exception as e: + err = f"Error: {e}" + st.session_state["messages"].append({"role": "assistant", "content": err}) + st.chat_message("assistant").write(err) diff --git a/Domains/AI-ML/MiniProjects/academic_chatbot/vectorstore/campus_faqs.faiss/index.faiss b/Domains/AI-ML/MiniProjects/academic_chatbot/vectorstore/campus_faqs.faiss/index.faiss new file mode 100755 index 00000000..d2abffb4 Binary files /dev/null and b/Domains/AI-ML/MiniProjects/academic_chatbot/vectorstore/campus_faqs.faiss/index.faiss differ diff --git a/Domains/AI-ML/MiniProjects/academic_chatbot/vectorstore/campus_faqs.faiss/index.pkl b/Domains/AI-ML/MiniProjects/academic_chatbot/vectorstore/campus_faqs.faiss/index.pkl new file mode 100755 index 00000000..10752405 Binary files /dev/null and b/Domains/AI-ML/MiniProjects/academic_chatbot/vectorstore/campus_faqs.faiss/index.pkl differ