YousefMohtady1 commited on
Commit
0ec978f
·
1 Parent(s): fedd57c

Initial commit with Database and PDF

Browse files
.env.example ADDED
@@ -0,0 +1 @@
 
 
1
+ GROQ_API_KEY=your_groq_api_key_here
.github/workflows/sync_to_hub.yml ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Sync to Hugging Face hub
2
+ on:
3
+ push:
4
+ branches: [main]
5
+
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ sync-to-hub:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v3
13
+ with:
14
+ fetch-depth: 0
15
+ lfs: true
16
+ - name: Push to hub
17
+ env:
18
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
19
+ run: |
20
+ git push https://YousefMohtady1:$HF_TOKEN@huggingface.co/spaces/YousefMohtady1/CorpGuideAI main
.gitignore ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ #uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+ #poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ #pdm.lock
116
+ #pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ #pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # SageMath parsed files
135
+ *.sage.py
136
+
137
+ # Environments
138
+ .env
139
+ .envrc
140
+ .venv
141
+ env/
142
+ venv/
143
+ ENV/
144
+ env.bak/
145
+ venv.bak/
146
+
147
+ # Spyder project settings
148
+ .spyderproject
149
+ .spyproject
150
+
151
+ # Rope project settings
152
+ .ropeproject
153
+
154
+ # mkdocs documentation
155
+ /site
156
+
157
+ # mypy
158
+ .mypy_cache/
159
+ .dmypy.json
160
+ dmypy.json
161
+
162
+ # Pyre type checker
163
+ .pyre/
164
+
165
+ # pytype static type analyzer
166
+ .pytype/
167
+
168
+ # Cython debug symbols
169
+ cython_debug/
170
+
171
+ # PyCharm
172
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
173
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
174
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
175
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
176
+ #.idea/
177
+
178
+ # Abstra
179
+ # Abstra is an AI-powered process automation framework.
180
+ # Ignore directories containing user credentials, local state, and settings.
181
+ # Learn more at https://abstra.io/docs
182
+ .abstra/
183
+
184
+ # Visual Studio Code
185
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
186
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
187
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
188
+ # you could uncomment the following to ignore the entire vscode folder
189
+ # .vscode/
190
+
191
+ # Ruff stuff:
192
+ .ruff_cache/
193
+
194
+ # PyPI configuration file
195
+ .pypirc
196
+
197
+ # Cursor
198
+ # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
199
+ # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
200
+ # refer to https://docs.cursor.com/context/ignore-files
201
+ .cursorignore
202
+ .cursorindexingignore
203
+
204
+ # Marimo
205
+ marimo/_static/
206
+ marimo/_lsp/
207
+ __marimo__/
Dockerfile ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+ WORKDIR /app
3
+
4
+ COPY requirements.txt .
5
+ RUN pip install --no-cache-dir -r requirements.txt
6
+
7
+ COPY . .
8
+
9
+ RUN chmod -R 777 .
10
+
11
+ EXPOSE 7860
12
+
13
+ CMD ["uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "7860"]
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Yousef Mohtady
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CorpGuideAI - HR Policy Assistant
2
+
3
+ **CorpGuideAI** is an advanced AI-powered assistant designed to help employees navigate and understand internal HR policies. Leveraging **Retrieval-Augmented Generation (RAG)**, it provides accurate, context-aware answers based on your organization's PDF documents.
4
+
5
+ ## 🚀 Features
6
+
7
+ - **RAG Architecture**: Combines vector search with generative AI for precise answers.
8
+ - **LLM Integration**: Powered by **Groq** (using Llama models) for fast and efficient inference.
9
+ - **Vector Database**: Uses **ChromaDB** for efficient document storage and retrieval.
10
+ - **Smart Ingestion**:
11
+ - Extracts text from PDFs.
12
+ - Improves retrieval speed and accuracy with semantic chunking.
13
+ - Uses `Alibaba-NLP/gte-multilingual-base` for robust multilingual support.
14
+ - **Interactive Web UI**: A modern, clean chat interface to interact with the assistant.
15
+ - **Chat History Management**: Maintains context across the conversation session (Reset supported).
16
+ - **FastAPI Backend**: A high-performance API to serve requests.
17
+ - **Docker Support**: Containerized for easy deployment.
18
+
19
+ ## 🛠️ Tech Stack
20
+
21
+ - **Language**: Python 3.10+
22
+ - **Frontend**: HTML5, CSS3, Vanilla JS
23
+ - **Backend**: FastAPI, Uvicorn
24
+ - **AI/ML**: LangChain, HuggingFace, ChromaDB, Groq
25
+ - **Tools**: `pypdf`, `sentence-transformers`, Docker
26
+
27
+ ## 📂 Project Structure
28
+
29
+ ```bash
30
+ CorpGuideAI-HR-Policy-Assistant/
31
+ ├── api/
32
+ │ ├── main.py # FastAPI application & entry point
33
+ │ ├── schemas.py # Pydantic models
34
+ ├── config/
35
+ │ ├── settings.py # Configuration settings
36
+ ├── core/
37
+ │ ├── rag_pipeline.py # Core RAG logic & Chat History
38
+ │ ├── prompts.py # Prompt templates
39
+ ├── data/ # PDF documents storage
40
+ ├── services/
41
+ │ ├── document_processor.py
42
+ │ ├── vector_store.py
43
+ │ ├── llm_client.py
44
+ ├── web_ui/ # Frontend Application
45
+ │ ├── index.html
46
+ │ ├── script.js
47
+ │ ├── style.css
48
+ ├── ingest.py # Document ingestion script
49
+ ├── Dockerfile # Docker container configuration
50
+ ├── requirements.txt # Dependencies
51
+ └── README.md # Documentation
52
+ ```
53
+
54
+ ## ⚡ Prerequisites
55
+
56
+ - **Python 3.10+**
57
+ - **Groq API Key**: Get it from [Groq Console](https://console.groq.com/).
58
+
59
+ ## 📦 Installation & Usage
60
+
61
+ ### Option 1: Local Installation
62
+
63
+ 1. **Clone & Setup**:
64
+
65
+ ```bash
66
+ git clone <repository-url>
67
+ cd CorpGuideAI-HR-Policy-Assistant
68
+ python -m venv venv
69
+
70
+ # Windows
71
+ venv\Scripts\activate
72
+ # macOS/Linux
73
+ source venv/bin/activate
74
+
75
+ pip install -r requirements.txt
76
+ ```
77
+
78
+ 2. **Environment Variables**:
79
+ Create a `.env` file:
80
+
81
+ ```env
82
+ GROQ_API_KEY=your_groq_api_key_here
83
+ ```
84
+
85
+ 3. **Ingest Documents**:
86
+ Place PDFs in `data/` and run:
87
+
88
+ ```bash
89
+ python ingest.py
90
+ ```
91
+
92
+ 4. **Run Server**:
93
+ ```bash
94
+ uvicorn api.main:app --reload
95
+ ```
96
+ Access the Web UI at: `http://localhost:8000`
97
+
98
+ ### Option 2: Docker
99
+
100
+ 1. **Build Image**:
101
+
102
+ ```bash
103
+ docker build -t corpguide-ai .
104
+ ```
105
+
106
+ 2. **Run Container**:
107
+ ```bash
108
+ docker run -p 7860:7860 --env-file .env corpguide-ai
109
+ ```
110
+ Access at: `http://localhost:7860`
111
+
112
+ ## 🔗 API Endpoints
113
+
114
+ - `GET /`: Serves the Web UI.
115
+ - `POST /chat`: Chat endpoint.
116
+ - Body: `{ "question": "..." }` (History managed internally)
117
+ - `POST /reset`: Clears current chat history.
118
+
119
+ ## 📄 License
120
+
121
+ MIT License - see [LICENSE](LICENSE) file.
api/__init__.py ADDED
File without changes
api/main.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from fastapi import FastAPI, HTTPException
3
+ from contextlib import asynccontextmanager
4
+ from fastapi.middleware.cors import CORSMiddleware
5
+ from fastapi.staticfiles import StaticFiles
6
+ from fastapi.responses import FileResponse
7
+ from api.schemas import ChatRequest, ChatResponse, UploadResponse
8
+ from core.rag_pipeline import RagPipeline
9
+
10
+ logging.basicConfig(level=logging.INFO)
11
+ logger = logging.getLogger(__name__)
12
+
13
+ pipeline_resources = {}
14
+
15
+ @asynccontextmanager
16
+ async def lifespan(app: FastAPI):
17
+ logger.info("Starting CorpGuide AI API...")
18
+ try:
19
+ pipeline_resources["rag"] = RagPipeline()
20
+ logger.info("CorpGuide AI API ready for queries")
21
+
22
+ except Exception as e:
23
+ logger.error(f"Failed to initialize CorpGuide AI API: {str(e)}")
24
+ raise e
25
+
26
+ yield
27
+
28
+ pipeline_resources.clear()
29
+ logger.info("API shut down.")
30
+
31
+ app = FastAPI(
32
+ title="CorpGuide AI",
33
+ description="AI-powered HR policy assistant",
34
+ version="1.0.0",
35
+ lifespan=lifespan
36
+ )
37
+
38
+ app.add_middleware(
39
+ CORSMiddleware,
40
+ allow_origins=["*"],
41
+ allow_credentials=True,
42
+ allow_methods=["*"],
43
+ allow_headers=["*"],
44
+ )
45
+
46
+ app.mount("/static", StaticFiles(directory="web_ui"), name="static")
47
+
48
+ @app.get("/")
49
+ async def read_index():
50
+ return FileResponse("web_ui/index.html")
51
+
52
+ @app.post("/reset")
53
+ async def reset_chat():
54
+ try:
55
+ if 'rag' in pipeline_resources and hasattr(pipeline_resources['rag'], 'clear_history'):
56
+ pipeline_resources['rag'].clear_history()
57
+ return {"message": "Chat history has been reset."}
58
+ except Exception as e:
59
+ raise HTTPException(status_code=500, detail=str(e))
60
+
61
+ @app.post("/chat", response_model=ChatResponse)
62
+ async def chat(request: ChatRequest):
63
+ try:
64
+ if 'rag' not in pipeline_resources:
65
+ raise HTTPException(status_code=503, detail="System is initializing, try again later")
66
+
67
+ rag = pipeline_resources['rag']
68
+
69
+ result = rag.process_query(
70
+ question= request.question,
71
+ chat_history=request.chat_history
72
+ )
73
+
74
+ return ChatResponse(
75
+ answer=result["answer"],
76
+ sources=result["sources"],
77
+ latency=result["latency"]
78
+ )
79
+
80
+ except Exception as e:
81
+ logger.error(f"Error in chat: {str(e)}")
82
+ raise HTTPException(status_code=500, detail=str(e))
api/schemas.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+ from typing import List, Optional, Tuple
3
+
4
+ class ChatRequest(BaseModel):
5
+ question: str
6
+ chat_history: List[Tuple[str,str]] = []
7
+
8
+ class ChatResponse(BaseModel):
9
+ answer:str
10
+ sources: List[str]
11
+ latency: Optional[float] = None
12
+
13
+ class UploadResponse(BaseModel):
14
+ filename: str
15
+ chunks_count: int
16
+ message:str
chroma_db/6a26d0e5-feba-4f8b-a92a-f8d1220144dc/data_level0.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c7e2a5c66a30e0d9228b85d06681048e2d25425ad5b7f8f10b672c87ac37e001
3
+ size 321200
chroma_db/6a26d0e5-feba-4f8b-a92a-f8d1220144dc/header.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:03cb3ac86f3e5bcb15e88b9bf99f760ec6b33e31d64a699e129b49868db6d733
3
+ size 100
chroma_db/6a26d0e5-feba-4f8b-a92a-f8d1220144dc/length.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ff6c03ed65fe1386e0c057caca3ac3771497dc92063f9713f93abdf9b6399b2a
3
+ size 400
chroma_db/6a26d0e5-feba-4f8b-a92a-f8d1220144dc/link_lists.bin ADDED
File without changes
chroma_db/chroma.sqlite3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d49857011870b48f6c378045d8d713fd1dac413de0a216d3003022472d80ea7f
3
+ size 1146880
config/__init.py ADDED
File without changes
config/settings.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+
4
+ load_dotenv()
5
+
6
+ class Settings:
7
+ #API Key
8
+ GROQ_API_KEY = os.getenv("GROQ_API_KEY")
9
+ if not GROQ_API_KEY:
10
+ raise ValueError("GROQ_API_KEY is not set in the environment variables")
11
+
12
+ #Models
13
+ LLM_MODEL = "meta-llama/llama-4-scout-17b-16e-instruct"
14
+ EMBEDDING_MODEL = "Alibaba-NLP/gte-multilingual-base"
15
+
16
+ #Vector DB
17
+ CHROMA_PERSIST_DIR = "chroma_db"
18
+ COLLECTION_NAME = "policy_docs"
19
+ DATA_DIR = "data"
20
+
21
+ settings = Settings()
core/__init.py ADDED
File without changes
core/prompts.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
2
+
3
+ SYSTEM_TEMPLATE = """
4
+ You are CorpGuide AI, an expert HR and Policy Assistant.
5
+ Your task is to answer the user's question based ONLY on the provided context below.
6
+
7
+ <context>
8
+ {context}
9
+ </context>
10
+
11
+ Guidelines:
12
+ 1. If the answer is not in the context, strictly reply: "I'm sorry, I cannot find this information in the company policy documents."
13
+ 2. Do not make up or hallucinate information.
14
+ 3. Keep your answer professional, concise, and helpful.
15
+ 4. If the question is in Arabic, answer in Arabic. If in English, answer in English.
16
+ 5. Provide specific details (numbers, days, penalties) if available in the context.
17
+ """
18
+
19
+ CONTEXTUALIZE_Q_SYSTEM_PROMPT = """
20
+ Given a chat history and the latest user question which might reference context in the chat history,
21
+ formulate a standalone question which can be understood without the chat history.
22
+ Do NOT answer the question, just reformulate it if needed and otherwise return it as is.
23
+ """
24
+
25
+ def get_chat_prompt():
26
+ return ChatPromptTemplate.from_messages([
27
+ ("system", SYSTEM_TEMPLATE),
28
+ MessagesPlaceholder("chat_history"),
29
+ ("user", "{input}")
30
+ ])
31
+
32
+ def get_contextualize_prompt():
33
+ return ChatPromptTemplate.from_messages([
34
+ ("system", CONTEXTUALIZE_Q_SYSTEM_PROMPT),
35
+ MessagesPlaceholder("chat_history"),
36
+ ("user", "{input}")
37
+ ])
core/rag_pipeline.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import time
3
+ from langchain_core.messages import HumanMessage, AIMessage
4
+ from langchain.chains import create_history_aware_retriever, create_retrieval_chain
5
+ from langchain.chains.combine_documents import create_stuff_documents_chain
6
+ from config.settings import Settings
7
+ from services.llm_client import LLMClient
8
+ from services.vector_store import VectorStore
9
+ from core.prompts import get_chat_prompt, get_contextualize_prompt
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ class RagPipeline:
14
+ def __init__(self):
15
+ try:
16
+ self.llm = LLMClient().get_llm()
17
+ self.vector_store = VectorStore()
18
+ self.retriever = self.vector_store.get_retriever(k=5)
19
+ self.prompt = get_chat_prompt()
20
+ self.history_aware_retriever = create_history_aware_retriever(
21
+ self.llm,
22
+ self.retriever,
23
+ get_contextualize_prompt()
24
+ )
25
+ self.question_answer_chain = create_stuff_documents_chain(
26
+ self.llm,
27
+ get_chat_prompt()
28
+ )
29
+ self.rag_chain = create_retrieval_chain(
30
+ self.history_aware_retriever,
31
+ self.question_answer_chain
32
+ )
33
+ self.chat_history = []
34
+ logger.info("RAG pipeline initialized successfully")
35
+
36
+ except Exception as e:
37
+ logger.error(f"Error initializing RAG pipeline: {str(e)}")
38
+ raise e
39
+
40
+ def clear_history(self):
41
+ self.chat_history = []
42
+ logger.info("Chat history cleared")
43
+
44
+ def process_query(self, question:str, chat_history: list = []):
45
+ start_time = time.time()
46
+ try:
47
+ logger.info(f"Processing query: {question}")
48
+ response = self.rag_chain.invoke({
49
+ "input": question,
50
+ "chat_history": self.chat_history
51
+ })
52
+
53
+ self.chat_history.extend([
54
+ HumanMessage(content=question),
55
+ AIMessage(content=response["answer"])
56
+ ])
57
+
58
+ latency = time.time() - start_time
59
+
60
+ source_files = list(set(
61
+ [doc.metadata.get("source", "Unknown") for doc in response["context"]]
62
+ ))
63
+
64
+ return{
65
+ "answer": response["answer"],
66
+ "sources": source_files,
67
+ "latency": latency
68
+ }
69
+
70
+ except Exception as e:
71
+ logger.error(f"Error processing query: {str(e)}")
72
+ raise e
data/employee_handbook_-_2024.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:60d7587505554f258c5758378243a21c4e0fa2484e730eee4c0e58aa245fde8c
3
+ size 314218
ingest.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import logging
3
+ import shutil
4
+ from services.document_processor import DocumentProcessor
5
+ from services.vector_store import VectorStore
6
+ from config.settings import Settings
7
+
8
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s- %(levelname)s - %(message)s')
9
+ logger = logging.getLogger(__name__)
10
+
11
+ def main():
12
+ logger.info("Starting document ingestion process")
13
+
14
+ if os.path.exists(Settings.CHROMA_PERSIST_DIR):
15
+ logger.warning(f"Removing existing database at {Settings.CHROMA_PERSIST_DIR}")
16
+ shutil.rmtree(Settings.CHROMA_PERSIST_DIR)
17
+
18
+ processor = DocumentProcessor()
19
+ vector_store = VectorStore()
20
+
21
+ if not os.path.exists(Settings.DATA_DIR):
22
+ os.makedirs(Settings.DATA_DIR)
23
+ logger.info(f"Data directory '{Settings.DATA_DIR}' not found. Created it. Please add PDFs there.")
24
+ return
25
+
26
+ pdf_files = [f for f in os.listdir(Settings.DATA_DIR) if f.endswith('.pdf')]
27
+
28
+ if not pdf_files:
29
+ logger.warning("No PDF files found in the data directory. Please add PDFs there.")
30
+ return
31
+
32
+ total_chunks = 0
33
+
34
+ for pdf_file in pdf_files:
35
+ file_path = os.path.join(Settings.DATA_DIR, pdf_file)
36
+ logger.info(f"Processing: {pdf_file}...")
37
+
38
+ try:
39
+ chunks = processor.process_pdf(file_path)
40
+
41
+ for chunk in chunks:
42
+ chunk.metadata['source'] = pdf_file
43
+
44
+ vector_store.add_documents(chunks)
45
+ total_chunks += len(chunks)
46
+ logger.info(f"Processed {len(chunks)} chunks from {pdf_file}")
47
+
48
+ except Exception as e:
49
+ logger .error(f"Failed to process {pdf_file}: {str(e)}")
50
+ continue
51
+
52
+ logger.info(f"Ingestion Completed. Total chunks stored: {total_chunks}")
53
+
54
+ if __name__ == "__main__":
55
+ main()
56
+
requirements.txt ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.124.4
2
+ uvicorn==0.38.0
3
+ python-multipart==0.0.20
4
+ python-dotenv==1.2.1
5
+ langchain==0.3.0
6
+ langchain-groq==0.2.0
7
+ langchain-huggingface==0.1.0
8
+ langchain-chroma==1.1.0
9
+ langchain-community==0.3.0
10
+ langchain-experimental==0.4.1
11
+ chromadb==1.3.7
12
+ pypdf==6.4.1
13
+ sentence-transformers==5.2.0
14
+ aiofiles==24.1.0
services/__init.py ADDED
File without changes
services/document_processor.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from typing import List
3
+ from langchain_core.documents import Document
4
+ from langchain_community.document_loaders import PyPDFLoader
5
+ from langchain_experimental.text_splitter import SemanticChunker
6
+ from langchain_huggingface import HuggingFaceEmbeddings
7
+ from config.settings import Settings
8
+
9
+ logger = logging.getLogger(__name__)
10
+ logging.basicConfig(level=logging.INFO)
11
+
12
+ class DocumentProcessor:
13
+ def __init__(self):
14
+ try:
15
+ logger.info(f"Loading embeddings model: {Settings.EMBEDDING_MODEL}")
16
+
17
+ # Initialize embeddings
18
+ self.embeddings = HuggingFaceEmbeddings(
19
+ model_name = Settings.EMBEDDING_MODEL,
20
+ model_kwargs = {"trust_remote_code": True}
21
+ )
22
+
23
+ # Initialize text Semantic splitter
24
+ self.text_splitter = SemanticChunker(
25
+ embeddings = self.embeddings,
26
+ breakpoint_threshold_type = "percentile"
27
+ )
28
+ logger.info("DocumentProcessor initialized successfully")
29
+
30
+ except Exception as e:
31
+ logger.error(f"Error initializing DocumentProcessor")
32
+ raise e
33
+
34
+ def process_pdf(self, file_path:str):
35
+ try:
36
+ logger.info(f"Processing file: {file_path}")
37
+
38
+ # Read PDF
39
+ loader = PyPDFLoader(file_path)
40
+ raw_documents = loader.load()
41
+ logger.info(f"Loaded {len(raw_documents)} pages from PDF.")
42
+
43
+ # Split text
44
+ chunks = self.text_splitter.split_documents(raw_documents)
45
+ logger.info(f"Created {len(chunks)} semantic chunks.")
46
+
47
+ return chunks
48
+
49
+ except Exception as e:
50
+ logger.error(f"Error processing PDF: {str(e)}")
51
+ raise e
services/llm_client.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from langchain_groq import ChatGroq
3
+ from config.settings import Settings
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+ class LLMClient:
8
+ def __init__(self):
9
+ try:
10
+ logger.info(f"Initializing Groq LLM with model: {Settings.LLM_MODEL}")
11
+
12
+ self.llm = ChatGroq(
13
+ api_key = Settings.GROQ_API_KEY,
14
+ model_name = Settings.LLM_MODEL,
15
+ temperature = 0.0,
16
+ max_retries = 2,
17
+ streaming = True
18
+ )
19
+ logger.info("LLM initialized successfully")
20
+
21
+ except Exception as e:
22
+ logger.error(f"Failed to initialize LLM Client: {str(e)}")
23
+ raise e
24
+
25
+ def get_llm(self):
26
+ return self.llm
services/vector_store.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import os
3
+ import shutil
4
+ from typing import List
5
+ from langchain_core.documents import Document
6
+ from langchain_chroma import Chroma
7
+ from langchain_huggingface import HuggingFaceEmbeddings
8
+ from config.settings import Settings
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ class VectorStore:
13
+ def __init__(self):
14
+ try:
15
+ self.embeddings = HuggingFaceEmbeddings(
16
+ model_name = Settings.EMBEDDING_MODEL,
17
+ model_kwargs = {"trust_remote_code": True}
18
+ )
19
+
20
+ self.vector_db = Chroma(
21
+ persist_directory = Settings.CHROMA_PERSIST_DIR,
22
+ embedding_function = self.embeddings,
23
+ collection_name = Settings.COLLECTION_NAME
24
+ )
25
+ logger.info(f"VectorStore connected to {Settings.CHROMA_PERSIST_DIR}")
26
+
27
+ except Exception as e:
28
+ logger.error(f"Failed to initialize VectorStore: {str(e)}")
29
+ raise e
30
+
31
+ def add_documents(self, documents: List[Document]):
32
+ try:
33
+ if not documents:
34
+ logger.warning("No documents provided to add to the VectorStore")
35
+ return
36
+
37
+ logger.info(f"Adding {len(documents)} documents to ChromaDB...")
38
+ self.vector_db.add_documents(documents)
39
+ logger.info("Documents added to ChromaDB successfully")
40
+
41
+ except Exception as e:
42
+ logger.error(f"Error adding documents: {str(e)}")
43
+ raise e
44
+
45
+ def get_retriever(self, k: int = 5):
46
+ return self.vector_db.as_retriever(
47
+ search_type = "similarity",
48
+ search_kwargs = {"k": k}
49
+ )
utils/__init.py ADDED
File without changes
utils/vision_helper.py ADDED
File without changes
web_ui/index.html ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en" dir="ltr">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>CorpGuide AI Assistant</title>
7
+ <link rel="stylesheet" href="/static/style.css" />
8
+ <link
9
+ rel="stylesheet"
10
+ href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"
11
+ />
12
+ </head>
13
+ <body>
14
+ <div class="chat-container">
15
+ <div class="chat-header">
16
+ <div class="logo">
17
+ <i class="fa-solid fa-robot"></i>
18
+ <span>CorpGuide AI</span>
19
+ </div>
20
+ <button
21
+ onclick="startNewChat()"
22
+ class="new-chat-btn"
23
+ title="Start New Chat"
24
+ >
25
+ <i class="fa-solid fa-arrows-rotate"></i>
26
+ </button>
27
+ <div class="status-dot"></div>
28
+ </div>
29
+
30
+ <div class="chat-box" id="chat-box">
31
+ <div class="message bot-message">
32
+ <div class="msg-content">
33
+ Hi, I am CorpGuide AI. Ask me any question about your company's
34
+ policies.
35
+ </div>
36
+ </div>
37
+ </div>
38
+
39
+ <div class="input-area">
40
+ <input
41
+ type="text"
42
+ id="user-input"
43
+ placeholder="Ask me any question about your company's policies."
44
+ autocomplete="off"
45
+ />
46
+ <button onclick="sendMessage()" id="send-btn">
47
+ <i class="fa-solid fa-paper-plane"></i>
48
+ </button>
49
+ </div>
50
+ </div>
51
+
52
+ <script src="/static/script.js"></script>
53
+ </body>
54
+ </html>
web_ui/script.js ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ==========================================
2
+ // Connection and Memory Settings
3
+ // ==========================================
4
+
5
+ // Relative URL to work both locally and on server
6
+ const API_URL = "/chat";
7
+ const RESET_URL = "/reset";
8
+
9
+ // Array to store chat history (so the model remembers context)
10
+ let chatHistory = [];
11
+
12
+ // Define page elements
13
+ const chatBox = document.getElementById('chat-box');
14
+ const userInput = document.getElementById('user-input');
15
+ const sendBtn = document.getElementById('send-btn');
16
+
17
+ // ==========================================
18
+ // Core Functions
19
+ // ==========================================
20
+
21
+ /**
22
+ * Function to send message and handle API interaction
23
+ */
24
+ async function sendMessage() {
25
+ const question = userInput.value.trim();
26
+
27
+ // If no text, do nothing
28
+ if (!question) return;
29
+
30
+ // 1. Display user message in chat immediately
31
+ appendMessage(question, 'user');
32
+ userInput.value = ''; // Clear input
33
+ sendBtn.disabled = true; // Disable button to prevent double sending
34
+
35
+ // 2. Display "typing..." indicator
36
+ const loadingId = appendLoading();
37
+
38
+ try {
39
+ // 3. Prepare payload (question + old history)
40
+ const payload = {
41
+ question: question,
42
+ chat_history: chatHistory
43
+ };
44
+
45
+ // 4. Connect to server
46
+ const response = await fetch(API_URL, {
47
+ method: 'POST',
48
+ headers: { 'Content-Type': 'application/json' },
49
+ body: JSON.stringify(payload)
50
+ });
51
+
52
+ if (!response.ok) {
53
+ throw new Error(`Server Error: ${response.status}`);
54
+ }
55
+
56
+ const data = await response.json();
57
+
58
+ // 5. Remove loading indicator and show AI response
59
+ removeLoading(loadingId);
60
+ appendMessage(data.answer, 'bot', data.sources);
61
+
62
+ // 6. Update memory (for next question)
63
+ // Add question and answer to the list
64
+ chatHistory.push(["human", question]);
65
+ chatHistory.push(["ai", data.answer]);
66
+
67
+ } catch (error) {
68
+ console.error("Error:", error);
69
+ removeLoading(loadingId);
70
+ appendMessage("Sorry, an error occurred connecting to the server. Please ensure the backend is running. 😔", 'bot');
71
+ } finally {
72
+ // Re-enable button and focus input
73
+ sendBtn.disabled = false;
74
+ userInput.focus();
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Function to start a new chat (Reset)
80
+ */
81
+ async function startNewChat() {
82
+ // 1. Reset browser memory
83
+ chatHistory = [];
84
+
85
+ // 2. Clear screen (return to initial state)
86
+ chatBox.innerHTML = `
87
+ <div class="message bot-message">
88
+ <div class="msg-content">
89
+ Welcome back! 👋<br>Memory cleared, you can start a new topic.
90
+ </div>
91
+ </div>
92
+ `;
93
+
94
+ // 3. Notify server to clear memory (optional, for double confirmation)
95
+ try {
96
+ await fetch(RESET_URL, { method: 'POST' });
97
+ console.log("Backend history reset.");
98
+ } catch (e) {
99
+ console.warn("Backend reset failed (might be stateless):", e);
100
+ }
101
+ }
102
+
103
+ // ==========================================
104
+ // UI Helpers
105
+ // ==========================================
106
+
107
+ /**
108
+ * Add message to screen
109
+ * @param {string} text - Message text
110
+ * @param {string} sender - Sender ('user' or 'bot')
111
+ * @param {Array} sources - List of sources (optional)
112
+ */
113
+ function appendMessage(text, sender, sources = []) {
114
+ const msgDiv = document.createElement('div');
115
+ msgDiv.classList.add('message', sender === 'user' ? 'user-message' : 'bot-message');
116
+
117
+ // Convert newlines to <br> for proper formatting
118
+ // Could use a library like 'marked' for full Markdown, but this is simpler
119
+ let formattedText = text.replace(/\n/g, '<br>');
120
+
121
+ // Convert text between ** ** to bold tags (simple implementation)
122
+ formattedText = formattedText.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
123
+
124
+ let html = `<div class="msg-content">${formattedText}</div>`;
125
+
126
+ /*
127
+ // If there are sources, add them in a small box below - DISABLED BY REQUEST
128
+ if (sources && sources.length > 0) {
129
+ // Remove duplicates from filenames
130
+ const uniqueSources = [...new Set(sources)];
131
+ html += `<div class="sources-box">📚 Sources: ${uniqueSources.join(', ')}</div>`;
132
+ }
133
+ */
134
+
135
+ msgDiv.innerHTML = html;
136
+ chatBox.appendChild(msgDiv);
137
+ scrollToBottom();
138
+ }
139
+
140
+ /**
141
+ * Add loading indicator (3 moving dots)
142
+ */
143
+ function appendLoading() {
144
+ const id = 'loading-' + Date.now();
145
+ const msgDiv = document.createElement('div');
146
+ msgDiv.classList.add('message', 'bot-message');
147
+ msgDiv.id = id;
148
+ msgDiv.innerHTML = `
149
+ <div class="msg-content">
150
+ <div class="typing-indicator">
151
+ <span></span><span></span><span></span>
152
+ </div>
153
+ </div>`;
154
+ chatBox.appendChild(msgDiv);
155
+ scrollToBottom();
156
+ return id;
157
+ }
158
+
159
+ /**
160
+ * Remove loading indicator
161
+ */
162
+ function removeLoading(id) {
163
+ const element = document.getElementById(id);
164
+ if (element) element.remove();
165
+ }
166
+
167
+ /**
168
+ * Scroll to bottom of chat automatically
169
+ */
170
+ function scrollToBottom() {
171
+ chatBox.scrollTop = chatBox.scrollHeight;
172
+ }
173
+
174
+ // ==========================================
175
+ // Event Listeners
176
+ // ==========================================
177
+
178
+ // When send button is clicked
179
+ sendBtn.addEventListener('click', sendMessage);
180
+
181
+ // When Enter is pressed in input box
182
+ userInput.addEventListener('keypress', (e) => {
183
+ if (e.key === 'Enter') {
184
+ sendMessage();
185
+ }
186
+ });
187
+
188
+ // Focus on input box when page loads
189
+ window.onload = () => userInput.focus();
web_ui/style.css ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ body {
2
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
3
+ background-color: #f0f2f5;
4
+ margin: 0;
5
+ display: flex;
6
+ justify-content: center;
7
+ align-items: center;
8
+ height: 100vh;
9
+ }
10
+
11
+ .chat-container {
12
+ width: 100%;
13
+ max-width: 500px; /* Suitable size for mobile and desktop */
14
+ background: white;
15
+ height: 90vh;
16
+ border-radius: 15px;
17
+ box-shadow: 0 10px 25px rgba(0,0,0,0.1);
18
+ display: flex;
19
+ flex-direction: column;
20
+ overflow: hidden;
21
+ }
22
+
23
+ .chat-header {
24
+ background: #2c3e50;
25
+ color: white;
26
+ padding: 15px;
27
+ display: flex;
28
+ align-items: center;
29
+ justify-content: space-between;
30
+ }
31
+
32
+ .new-chat-btn {
33
+ background: transparent;
34
+ border: 1px solid rgba(255, 255, 255, 0.3);
35
+ color: white;
36
+ width: 35px;
37
+ height: 35px;
38
+ border-radius: 50%;
39
+ cursor: pointer;
40
+ display: flex;
41
+ align-items: center;
42
+ justify-content: center;
43
+ transition: all 0.3s ease;
44
+ }
45
+
46
+ .new-chat-btn:hover {
47
+ background: rgba(255, 255, 255, 0.2);
48
+ transform: rotate(180deg); /* Cool rotation effect on hover */
49
+ }
50
+
51
+ .logo { font-size: 1.2rem; font-weight: bold; }
52
+ .logo i { margin-left: 10px; color: #3498db; }
53
+ .status-dot { width: 10px; height: 10px; background: #2ecc71; border-radius: 50%; }
54
+
55
+ .chat-box {
56
+ flex: 1;
57
+ padding: 20px;
58
+ overflow-y: auto;
59
+ background: #f9f9f9;
60
+ display: flex;
61
+ flex-direction: column;
62
+ gap: 15px;
63
+ }
64
+
65
+ .message { display: flex; flex-direction: column; max-width: 80%; }
66
+ .bot-message { align-self: flex-start; }
67
+ .user-message { align-self: flex-end; }
68
+
69
+ .msg-content {
70
+ padding: 12px 16px;
71
+ border-radius: 15px;
72
+ font-size: 0.95rem;
73
+ line-height: 1.5;
74
+ position: relative;
75
+ }
76
+
77
+ .bot-message .msg-content {
78
+ background: #e9ecef;
79
+ color: #333;
80
+ border-bottom-right-radius: 2px;
81
+ }
82
+
83
+ .user-message .msg-content {
84
+ background: #3498db;
85
+ color: white;
86
+ border-bottom-left-radius: 2px;
87
+ }
88
+
89
+ .sources-box {
90
+ font-size: 0.8rem;
91
+ color: #666;
92
+ margin-top: 5px;
93
+ background: #fff;
94
+ padding: 5px 10px;
95
+ border-radius: 5px;
96
+ border: 1px solid #ddd;
97
+ }
98
+
99
+ .input-area {
100
+ padding: 15px;
101
+ background: white;
102
+ border-top: 1px solid #eee;
103
+ display: flex;
104
+ gap: 10px;
105
+ }
106
+
107
+ input {
108
+ flex: 1;
109
+ padding: 12px;
110
+ border: 1px solid #ddd;
111
+ border-radius: 25px;
112
+ outline: none;
113
+ font-family: inherit;
114
+ }
115
+
116
+ button {
117
+ background: #2c3e50;
118
+ color: white;
119
+ border: none;
120
+ width: 45px;
121
+ height: 45px;
122
+ border-radius: 50%;
123
+ cursor: pointer;
124
+ transition: 0.2s;
125
+ }
126
+
127
+ button:hover { background: #34495e; }
128
+ button:disabled { background: #ccc; cursor: not-allowed; }
129
+
130
+ /* Loading Animation */
131
+ .typing-indicator span {
132
+ display: inline-block;
133
+ width: 6px; height: 6px;
134
+ background-color: #555;
135
+ border-radius: 50%;
136
+ animation: typing 1s infinite;
137
+ margin: 0 2px;
138
+ }
139
+ @keyframes typing { 0% {opacity: 0.3} 50% {opacity: 1} 100% {opacity: 0.3} }