Martechsol commited on
Commit ·
d6ca65c
1
Parent(s): e33f2d7
Final sync: Ensure all intelligence and formatting fixes are pushed
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- backup_2026-05-04/.env.example +15 -0
- backup_2026-05-04/.gitattributes +2 -0
- backup_2026-05-04/.gitignore +9 -0
- backup_2026-05-04/CONFIG_NOTES.md +38 -0
- backup_2026-05-04/CUSTOMIZATION_GUIDE.md +55 -0
- backup_2026-05-04/Dockerfile +31 -0
- backup_2026-05-04/PHPMailer/Exception.php +40 -0
- backup_2026-05-04/PHPMailer/PHPMailer.php +0 -0
- backup_2026-05-04/PHPMailer/SMTP.php +1509 -0
- backup_2026-05-04/PROJECT_OVERVIEW.md +32 -0
- backup_2026-05-04/README.md +164 -0
- backup_2026-05-04/app.py +6 -0
- backup_2026-05-04/app/__init__.py +0 -0
- backup_2026-05-04/app/admin/__init__.py +1 -0
- backup_2026-05-04/app/admin/router.py +346 -0
- backup_2026-05-04/app/admin/templates/admin.html +1143 -0
- backup_2026-05-04/app/api/__init__.py +0 -0
- backup_2026-05-04/app/api/schemas.py +27 -0
- backup_2026-05-04/app/core/__init__.py +0 -0
- backup_2026-05-04/app/core/config.py +51 -0
- backup_2026-05-04/app/main.py +196 -0
- backup_2026-05-04/app/services/__init__.py +0 -0
- backup_2026-05-04/app/services/chunker.py +39 -0
- backup_2026-05-04/app/services/document_loader.py +37 -0
- backup_2026-05-04/app/services/embeddings.py +34 -0
- backup_2026-05-04/app/services/llm.py +264 -0
- backup_2026-05-04/app/services/rag_pipeline.py +98 -0
- backup_2026-05-04/app/services/rate_limiter.py +24 -0
- backup_2026-05-04/app/services/reranker.py +26 -0
- backup_2026-05-04/app/services/session_store.py +284 -0
- backup_2026-05-04/app/services/vector_store.py +167 -0
- backup_2026-05-04/app/ui_gradio.py +275 -0
- backup_2026-05-04/docker-compose.yml +14 -0
- backup_2026-05-04/docs/MartechSol_Employee_HandBook (7) (1).pdf +3 -0
- backup_2026-05-04/docs/MartechSol_Employee_Handbook_Extracted.txt +1091 -0
- backup_2026-05-04/floaitng-icon/KhloeSAC.lottie +3 -0
- backup_2026-05-04/get_models.py +25 -0
- backup_2026-05-04/martech_sol_logo.jpg +0 -0
- backup_2026-05-04/requirements.txt +14 -0
- backup_2026-05-04/static/addon.html +543 -0
- backup_2026-05-04/static/bot-icon.lottie +3 -0
- backup_2026-05-04/static/chat-loader.js +48 -0
- backup_2026-05-04/test_groq.py +26 -0
- backup_2026-05-04/wordpress code/WP_INTEGRATION_SCRIPT.txt +12 -0
- backup_2026-05-04/wordpress code/addon.html +477 -0
- backup_2026-05-04/wordpress code/addon_backup.html +175 -0
- backup_2026-05-13_pre_intelligent_retrieval/.env.example +19 -0
- backup_2026-05-13_pre_intelligent_retrieval/.gitattributes +2 -0
- backup_2026-05-13_pre_intelligent_retrieval/.gitignore +9 -0
- backup_2026-05-13_pre_intelligent_retrieval/CONFIG_NOTES.md +38 -0
backup_2026-05-04/.env.example
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
LLM_PROVIDER=groq
|
| 2 |
+
GROQ_API_KEY=your_groq_api_key
|
| 3 |
+
GROQ_MODEL=llama-3.1-8b-instant
|
| 4 |
+
HF_API_KEY=your_huggingface_api_key
|
| 5 |
+
HF_MODEL=meta-llama/Llama-3.1-8B-Instruct
|
| 6 |
+
EMBEDDING_MODEL=BAAI/bge-small-en-v1.5
|
| 7 |
+
DOCS_DIR=docs
|
| 8 |
+
INDEX_DIR=data/index
|
| 9 |
+
SESSIONS_DIR=data/sessions
|
| 10 |
+
TOP_K=4
|
| 11 |
+
CORS_ALLOW_ORIGINS=*
|
| 12 |
+
API_KEY=
|
| 13 |
+
RATE_LIMIT_REQUESTS=60
|
| 14 |
+
RATE_LIMIT_WINDOW_SECONDS=60
|
| 15 |
+
RAG_API_URL=
|
backup_2026-05-04/.gitattributes
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.lottie filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.pdf filter=lfs diff=lfs merge=lfs -text
|
backup_2026-05-04/.gitignore
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.env
|
| 2 |
+
__pycache__/
|
| 3 |
+
.pytest_cache/
|
| 4 |
+
.mypy_cache/
|
| 5 |
+
.venv/
|
| 6 |
+
venv/
|
| 7 |
+
# Ignoring data directory to prevent pushing binary index files
|
| 8 |
+
data/index/
|
| 9 |
+
data/sessions/
|
backup_2026-05-04/CONFIG_NOTES.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Configuration Backup - Martechsol RAG Chatbot
|
| 2 |
+
**Date:** 2026-04-28
|
| 3 |
+
**Source:** C:\Users\DELL\Desktop\RAG_backup2
|
| 4 |
+
**Destination:** D:\RAG_Backup_2026_04_28
|
| 5 |
+
|
| 6 |
+
## Core Settings (from app/core/config.py)
|
| 7 |
+
- **App Name:** Fast RAG Chatbot
|
| 8 |
+
- **LLM Provider:** groq (default)
|
| 9 |
+
- **Groq Model:** llama-3.1-8b-instant
|
| 10 |
+
- **HF Model:** meta-llama/Llama-3.1-8B-Instruct
|
| 11 |
+
- **Embedding Model:** BAAI/bge-small-en-v1.5
|
| 12 |
+
- **Docs Directory:** docs/
|
| 13 |
+
- **Index Directory:** data/index/
|
| 14 |
+
- **Sessions Directory:** data/sessions/
|
| 15 |
+
- **Chunk Size:** 420 tokens
|
| 16 |
+
- **Overlap:** 80 tokens
|
| 17 |
+
- **Top K:** 4
|
| 18 |
+
|
| 19 |
+
## Admin Credentials
|
| 20 |
+
- **Username:** martech_admin
|
| 21 |
+
- **Password:** martech_admin_303
|
| 22 |
+
- **OTP Expiry:** 300 seconds (5 minutes)
|
| 23 |
+
|
| 24 |
+
## Security
|
| 25 |
+
- **Auth Method:** HTTP Basic Auth + OTP (email-based)
|
| 26 |
+
- **Email for OTP:** randomjoedown@gmail.com
|
| 27 |
+
- **SMTP Server:** smtp.gmail.com (Port 465)
|
| 28 |
+
|
| 29 |
+
## API / Integration
|
| 30 |
+
- **Endpoint:** /api/chat
|
| 31 |
+
- **Widget Endpoint:** /widget
|
| 32 |
+
- **Admin Endpoint:** /admin
|
| 33 |
+
- **CORS:** Allowed for all (*) for testing
|
| 34 |
+
|
| 35 |
+
## Infrastructure
|
| 36 |
+
- **Hugging Face Path:** /data (for persistent storage)
|
| 37 |
+
- **Local Path:** data/ (for local storage)
|
| 38 |
+
- **Dockerfile:** Based on python:3.10-slim, serves on port 7860
|
backup_2026-05-04/CUSTOMIZATION_GUIDE.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Martechsol Assistant Customization Guide
|
| 2 |
+
|
| 3 |
+
This guide explains how to customize the visual appearance and behavior of your Gradio-based chat interface.
|
| 4 |
+
|
| 5 |
+
## 1. Modifying the UI Layout (app/ui_gradio.py)
|
| 6 |
+
|
| 7 |
+
The interface is built using `gradio.Blocks`. You can modify the structure in the `with gr.Blocks(...) as demo:` section.
|
| 8 |
+
|
| 9 |
+
### Common Components:
|
| 10 |
+
- **`chatbot`**: The main chat window. We use `elem_id="chatbot-window"` for CSS targeting.
|
| 11 |
+
- **`msg`**: The text input box.
|
| 12 |
+
- **`send`**: The primary action button.
|
| 13 |
+
|
| 14 |
+
## 2. Custom CSS (app/ui_gradio.py)
|
| 15 |
+
|
| 16 |
+
At the top of `app/ui_gradio.py`, there is a `custom_css` string. This is the most powerful way to change the look of your app.
|
| 17 |
+
|
| 18 |
+
### How to target elements:
|
| 19 |
+
We use `elem_id` in Python to give elements stable names.
|
| 20 |
+
|
| 21 |
+
Example:
|
| 22 |
+
```python
|
| 23 |
+
chatbot = gr.Chatbot(elem_id="my-custom-chat")
|
| 24 |
+
```
|
| 25 |
+
Then in CSS:
|
| 26 |
+
```css
|
| 27 |
+
#my-custom-chat {
|
| 28 |
+
background-color: #f0f0f0;
|
| 29 |
+
}
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
### Important Selectors:
|
| 33 |
+
- **`footer`**: Targets the Gradio branding at the bottom.
|
| 34 |
+
- **`.gradio-container`**: The main outer wrapper of your app.
|
| 35 |
+
- **`[data-testid="user"]`**: Targets user messages.
|
| 36 |
+
- **`[data-testid="bot"]`**: Targets assistant messages.
|
| 37 |
+
|
| 38 |
+
## 3. Persistent Data on Hugging Face
|
| 39 |
+
|
| 40 |
+
Your data is stored in the `/data` volume.
|
| 41 |
+
- **Sessions**: `/data/sessions/`
|
| 42 |
+
- **FAISS Index**: `/data/index/`
|
| 43 |
+
|
| 44 |
+
To manage these, you can use the **Admin Panel** at `/admin`.
|
| 45 |
+
|
| 46 |
+
## 4. Tips for Widget Embedding
|
| 47 |
+
|
| 48 |
+
If you are embedding this as an iframe (like in `addon.html`):
|
| 49 |
+
1. **Remove redundant headers**: We hide the Markdown titles in the CSS (`#title-area { display: none; }`) so the iframe looks like a part of your page.
|
| 50 |
+
2. **Handle height**: We set `height: auto !important` and `min-height: 40px !important` on the chatbot so it doesn't take up 500px by default.
|
| 51 |
+
3. **Transparent Background**: You can add `body { background-color: transparent !important; }` to the `custom_css` if your theme supports it.
|
| 52 |
+
|
| 53 |
+
## 5. Deployment
|
| 54 |
+
|
| 55 |
+
Whenever you save a file, the changes are automatically pushed to your Hugging Face Space. The Space will rebuild automatically (takes about 1-2 minutes).
|
backup_2026-05-04/Dockerfile
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
# Set environment variables
|
| 4 |
+
ENV PYTHONDONTWRITEBYTECODE=1
|
| 5 |
+
ENV PYTHONUNBUFFERED=1
|
| 6 |
+
ENV HOME=/home/user
|
| 7 |
+
|
| 8 |
+
# Create a non-root user for Hugging Face
|
| 9 |
+
RUN useradd -m -u 1000 user
|
| 10 |
+
WORKDIR $HOME/app
|
| 11 |
+
|
| 12 |
+
# Pre-create data directories with correct ownership
|
| 13 |
+
RUN mkdir -p $HOME/app/data/index $HOME/app/data/sessions && \
|
| 14 |
+
chown -R user:user $HOME/app
|
| 15 |
+
|
| 16 |
+
# Switch to non-root user
|
| 17 |
+
USER user
|
| 18 |
+
ENV PATH=$HOME/.local/bin:$PATH
|
| 19 |
+
|
| 20 |
+
# Install dependencies
|
| 21 |
+
COPY --chown=user requirements.txt .
|
| 22 |
+
RUN pip install --no-cache-dir --user -r requirements.txt
|
| 23 |
+
|
| 24 |
+
# Copy application code
|
| 25 |
+
COPY --chown=user . .
|
| 26 |
+
|
| 27 |
+
# Expose port
|
| 28 |
+
EXPOSE 7860
|
| 29 |
+
|
| 30 |
+
# Start application
|
| 31 |
+
CMD ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "1"]
|
backup_2026-05-04/PHPMailer/Exception.php
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* PHPMailer Exception class.
|
| 5 |
+
* PHP Version 5.5.
|
| 6 |
+
*
|
| 7 |
+
* @see https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project
|
| 8 |
+
*
|
| 9 |
+
* @author Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk>
|
| 10 |
+
* @author Jim Jagielski (jimjag) <jimjag@gmail.com>
|
| 11 |
+
* @author Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net>
|
| 12 |
+
* @author Brent R. Matzelle (original founder)
|
| 13 |
+
* @copyright 2012 - 2020 Marcus Bointon
|
| 14 |
+
* @copyright 2010 - 2012 Jim Jagielski
|
| 15 |
+
* @copyright 2004 - 2009 Andy Prevost
|
| 16 |
+
* @license https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html GNU Lesser General Public License
|
| 17 |
+
* @note This program is distributed in the hope that it will be useful - WITHOUT
|
| 18 |
+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
| 19 |
+
* FITNESS FOR A PARTICULAR PURPOSE.
|
| 20 |
+
*/
|
| 21 |
+
|
| 22 |
+
namespace PHPMailer\PHPMailer;
|
| 23 |
+
|
| 24 |
+
/**
|
| 25 |
+
* PHPMailer exception handler.
|
| 26 |
+
*
|
| 27 |
+
* @author Marcus Bointon <phpmailer@synchromedia.co.uk>
|
| 28 |
+
*/
|
| 29 |
+
class Exception extends \Exception
|
| 30 |
+
{
|
| 31 |
+
/**
|
| 32 |
+
* Prettify error message output.
|
| 33 |
+
*
|
| 34 |
+
* @return string
|
| 35 |
+
*/
|
| 36 |
+
public function errorMessage()
|
| 37 |
+
{
|
| 38 |
+
return '<strong>' . htmlspecialchars($this->getMessage(), ENT_COMPAT | ENT_HTML401) . "</strong><br />\n";
|
| 39 |
+
}
|
| 40 |
+
}
|
backup_2026-05-04/PHPMailer/PHPMailer.php
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
backup_2026-05-04/PHPMailer/SMTP.php
ADDED
|
@@ -0,0 +1,1509 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* PHPMailer RFC821 SMTP email transport class.
|
| 5 |
+
* PHP Version 5.5.
|
| 6 |
+
*
|
| 7 |
+
* @see https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project
|
| 8 |
+
*
|
| 9 |
+
* @author Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk>
|
| 10 |
+
* @author Jim Jagielski (jimjag) <jimjag@gmail.com>
|
| 11 |
+
* @author Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net>
|
| 12 |
+
* @author Brent R. Matzelle (original founder)
|
| 13 |
+
* @copyright 2012 - 2020 Marcus Bointon
|
| 14 |
+
* @copyright 2010 - 2012 Jim Jagielski
|
| 15 |
+
* @copyright 2004 - 2009 Andy Prevost
|
| 16 |
+
* @license https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html GNU Lesser General Public License
|
| 17 |
+
* @note This program is distributed in the hope that it will be useful - WITHOUT
|
| 18 |
+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
| 19 |
+
* FITNESS FOR A PARTICULAR PURPOSE.
|
| 20 |
+
*/
|
| 21 |
+
|
| 22 |
+
namespace PHPMailer\PHPMailer;
|
| 23 |
+
|
| 24 |
+
/**
|
| 25 |
+
* PHPMailer RFC821 SMTP email transport class.
|
| 26 |
+
* Implements RFC 821 SMTP commands and provides some utility methods for sending mail to an SMTP server.
|
| 27 |
+
*
|
| 28 |
+
* @author Chris Ryan
|
| 29 |
+
* @author Marcus Bointon <phpmailer@synchromedia.co.uk>
|
| 30 |
+
*/
|
| 31 |
+
class SMTP
|
| 32 |
+
{
|
| 33 |
+
/**
|
| 34 |
+
* The PHPMailer SMTP version number.
|
| 35 |
+
*
|
| 36 |
+
* @var string
|
| 37 |
+
*/
|
| 38 |
+
const VERSION = '6.9.3';
|
| 39 |
+
|
| 40 |
+
/**
|
| 41 |
+
* SMTP line break constant.
|
| 42 |
+
*
|
| 43 |
+
* @var string
|
| 44 |
+
*/
|
| 45 |
+
const LE = "\r\n";
|
| 46 |
+
|
| 47 |
+
/**
|
| 48 |
+
* The SMTP port to use if one is not specified.
|
| 49 |
+
*
|
| 50 |
+
* @var int
|
| 51 |
+
*/
|
| 52 |
+
const DEFAULT_PORT = 25;
|
| 53 |
+
|
| 54 |
+
/**
|
| 55 |
+
* The SMTPs port to use if one is not specified.
|
| 56 |
+
*
|
| 57 |
+
* @var int
|
| 58 |
+
*/
|
| 59 |
+
const DEFAULT_SECURE_PORT = 465;
|
| 60 |
+
|
| 61 |
+
/**
|
| 62 |
+
* The maximum line length allowed by RFC 5321 section 4.5.3.1.6,
|
| 63 |
+
* *excluding* a trailing CRLF break.
|
| 64 |
+
*
|
| 65 |
+
* @see https://www.rfc-editor.org/rfc/rfc5321#section-4.5.3.1.6
|
| 66 |
+
*
|
| 67 |
+
* @var int
|
| 68 |
+
*/
|
| 69 |
+
const MAX_LINE_LENGTH = 998;
|
| 70 |
+
|
| 71 |
+
/**
|
| 72 |
+
* The maximum line length allowed for replies in RFC 5321 section 4.5.3.1.5,
|
| 73 |
+
* *including* a trailing CRLF line break.
|
| 74 |
+
*
|
| 75 |
+
* @see https://www.rfc-editor.org/rfc/rfc5321#section-4.5.3.1.5
|
| 76 |
+
*
|
| 77 |
+
* @var int
|
| 78 |
+
*/
|
| 79 |
+
const MAX_REPLY_LENGTH = 512;
|
| 80 |
+
|
| 81 |
+
/**
|
| 82 |
+
* Debug level for no output.
|
| 83 |
+
*
|
| 84 |
+
* @var int
|
| 85 |
+
*/
|
| 86 |
+
const DEBUG_OFF = 0;
|
| 87 |
+
|
| 88 |
+
/**
|
| 89 |
+
* Debug level to show client -> server messages.
|
| 90 |
+
*
|
| 91 |
+
* @var int
|
| 92 |
+
*/
|
| 93 |
+
const DEBUG_CLIENT = 1;
|
| 94 |
+
|
| 95 |
+
/**
|
| 96 |
+
* Debug level to show client -> server and server -> client messages.
|
| 97 |
+
*
|
| 98 |
+
* @var int
|
| 99 |
+
*/
|
| 100 |
+
const DEBUG_SERVER = 2;
|
| 101 |
+
|
| 102 |
+
/**
|
| 103 |
+
* Debug level to show connection status, client -> server and server -> client messages.
|
| 104 |
+
*
|
| 105 |
+
* @var int
|
| 106 |
+
*/
|
| 107 |
+
const DEBUG_CONNECTION = 3;
|
| 108 |
+
|
| 109 |
+
/**
|
| 110 |
+
* Debug level to show all messages.
|
| 111 |
+
*
|
| 112 |
+
* @var int
|
| 113 |
+
*/
|
| 114 |
+
const DEBUG_LOWLEVEL = 4;
|
| 115 |
+
|
| 116 |
+
/**
|
| 117 |
+
* Debug output level.
|
| 118 |
+
* Options:
|
| 119 |
+
* * self::DEBUG_OFF (`0`) No debug output, default
|
| 120 |
+
* * self::DEBUG_CLIENT (`1`) Client commands
|
| 121 |
+
* * self::DEBUG_SERVER (`2`) Client commands and server responses
|
| 122 |
+
* * self::DEBUG_CONNECTION (`3`) As DEBUG_SERVER plus connection status
|
| 123 |
+
* * self::DEBUG_LOWLEVEL (`4`) Low-level data output, all messages.
|
| 124 |
+
*
|
| 125 |
+
* @var int
|
| 126 |
+
*/
|
| 127 |
+
public $do_debug = self::DEBUG_OFF;
|
| 128 |
+
|
| 129 |
+
/**
|
| 130 |
+
* How to handle debug output.
|
| 131 |
+
* Options:
|
| 132 |
+
* * `echo` Output plain-text as-is, appropriate for CLI
|
| 133 |
+
* * `html` Output escaped, line breaks converted to `<br>`, appropriate for browser output
|
| 134 |
+
* * `error_log` Output to error log as configured in php.ini
|
| 135 |
+
* Alternatively, you can provide a callable expecting two params: a message string and the debug level:
|
| 136 |
+
*
|
| 137 |
+
* ```php
|
| 138 |
+
* $smtp->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";};
|
| 139 |
+
* ```
|
| 140 |
+
*
|
| 141 |
+
* Alternatively, you can pass in an instance of a PSR-3 compatible logger, though only `debug`
|
| 142 |
+
* level output is used:
|
| 143 |
+
*
|
| 144 |
+
* ```php
|
| 145 |
+
* $mail->Debugoutput = new myPsr3Logger;
|
| 146 |
+
* ```
|
| 147 |
+
*
|
| 148 |
+
* @var string|callable|\Psr\Log\LoggerInterface
|
| 149 |
+
*/
|
| 150 |
+
public $Debugoutput = 'echo';
|
| 151 |
+
|
| 152 |
+
/**
|
| 153 |
+
* Whether to use VERP.
|
| 154 |
+
*
|
| 155 |
+
* @see https://en.wikipedia.org/wiki/Variable_envelope_return_path
|
| 156 |
+
* @see https://www.postfix.org/VERP_README.html Info on VERP
|
| 157 |
+
*
|
| 158 |
+
* @var bool
|
| 159 |
+
*/
|
| 160 |
+
public $do_verp = false;
|
| 161 |
+
|
| 162 |
+
/**
|
| 163 |
+
* The timeout value for connection, in seconds.
|
| 164 |
+
* Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2.
|
| 165 |
+
* This needs to be quite high to function correctly with hosts using greetdelay as an anti-spam measure.
|
| 166 |
+
*
|
| 167 |
+
* @see https://www.rfc-editor.org/rfc/rfc2821#section-4.5.3.2
|
| 168 |
+
*
|
| 169 |
+
* @var int
|
| 170 |
+
*/
|
| 171 |
+
public $Timeout = 300;
|
| 172 |
+
|
| 173 |
+
/**
|
| 174 |
+
* How long to wait for commands to complete, in seconds.
|
| 175 |
+
* Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2.
|
| 176 |
+
*
|
| 177 |
+
* @var int
|
| 178 |
+
*/
|
| 179 |
+
public $Timelimit = 300;
|
| 180 |
+
|
| 181 |
+
/**
|
| 182 |
+
* Patterns to extract an SMTP transaction id from reply to a DATA command.
|
| 183 |
+
* The first capture group in each regex will be used as the ID.
|
| 184 |
+
* MS ESMTP returns the message ID, which may not be correct for internal tracking.
|
| 185 |
+
*
|
| 186 |
+
* @var string[]
|
| 187 |
+
*/
|
| 188 |
+
protected $smtp_transaction_id_patterns = [
|
| 189 |
+
'exim' => '/[\d]{3} OK id=(.*)/',
|
| 190 |
+
'sendmail' => '/[\d]{3} 2\.0\.0 (.*) Message/',
|
| 191 |
+
'postfix' => '/[\d]{3} 2\.0\.0 Ok: queued as (.*)/',
|
| 192 |
+
'Microsoft_ESMTP' => '/[0-9]{3} 2\.[\d]\.0 (.*)@(?:.*) Queued mail for delivery/',
|
| 193 |
+
'Amazon_SES' => '/[\d]{3} Ok (.*)/',
|
| 194 |
+
'SendGrid' => '/[\d]{3} Ok: queued as (.*)/',
|
| 195 |
+
'CampaignMonitor' => '/[\d]{3} 2\.0\.0 OK:([a-zA-Z\d]{48})/',
|
| 196 |
+
'Haraka' => '/[\d]{3} Message Queued \((.*)\)/',
|
| 197 |
+
'ZoneMTA' => '/[\d]{3} Message queued as (.*)/',
|
| 198 |
+
'Mailjet' => '/[\d]{3} OK queued as (.*)/',
|
| 199 |
+
];
|
| 200 |
+
|
| 201 |
+
/**
|
| 202 |
+
* Allowed SMTP XCLIENT attributes.
|
| 203 |
+
* Must be allowed by the SMTP server. EHLO response is not checked.
|
| 204 |
+
*
|
| 205 |
+
* @see https://www.postfix.org/XCLIENT_README.html
|
| 206 |
+
*
|
| 207 |
+
* @var array
|
| 208 |
+
*/
|
| 209 |
+
public static $xclient_allowed_attributes = [
|
| 210 |
+
'NAME', 'ADDR', 'PORT', 'PROTO', 'HELO', 'LOGIN', 'DESTADDR', 'DESTPORT'
|
| 211 |
+
];
|
| 212 |
+
|
| 213 |
+
/**
|
| 214 |
+
* The last transaction ID issued in response to a DATA command,
|
| 215 |
+
* if one was detected.
|
| 216 |
+
*
|
| 217 |
+
* @var string|bool|null
|
| 218 |
+
*/
|
| 219 |
+
protected $last_smtp_transaction_id;
|
| 220 |
+
|
| 221 |
+
/**
|
| 222 |
+
* The socket for the server connection.
|
| 223 |
+
*
|
| 224 |
+
* @var ?resource
|
| 225 |
+
*/
|
| 226 |
+
protected $smtp_conn;
|
| 227 |
+
|
| 228 |
+
/**
|
| 229 |
+
* Error information, if any, for the last SMTP command.
|
| 230 |
+
*
|
| 231 |
+
* @var array
|
| 232 |
+
*/
|
| 233 |
+
protected $error = [
|
| 234 |
+
'error' => '',
|
| 235 |
+
'detail' => '',
|
| 236 |
+
'smtp_code' => '',
|
| 237 |
+
'smtp_code_ex' => '',
|
| 238 |
+
];
|
| 239 |
+
|
| 240 |
+
/**
|
| 241 |
+
* The reply the server sent to us for HELO.
|
| 242 |
+
* If null, no HELO string has yet been received.
|
| 243 |
+
*
|
| 244 |
+
* @var string|null
|
| 245 |
+
*/
|
| 246 |
+
protected $helo_rply;
|
| 247 |
+
|
| 248 |
+
/**
|
| 249 |
+
* The set of SMTP extensions sent in reply to EHLO command.
|
| 250 |
+
* Indexes of the array are extension names.
|
| 251 |
+
* Value at index 'HELO' or 'EHLO' (according to command that was sent)
|
| 252 |
+
* represents the server name. In case of HELO it is the only element of the array.
|
| 253 |
+
* Other values can be boolean TRUE or an array containing extension options.
|
| 254 |
+
* If null, no HELO/EHLO string has yet been received.
|
| 255 |
+
*
|
| 256 |
+
* @var array|null
|
| 257 |
+
*/
|
| 258 |
+
protected $server_caps;
|
| 259 |
+
|
| 260 |
+
/**
|
| 261 |
+
* The most recent reply received from the server.
|
| 262 |
+
*
|
| 263 |
+
* @var string
|
| 264 |
+
*/
|
| 265 |
+
protected $last_reply = '';
|
| 266 |
+
|
| 267 |
+
/**
|
| 268 |
+
* Output debugging info via a user-selected method.
|
| 269 |
+
*
|
| 270 |
+
* @param string $str Debug string to output
|
| 271 |
+
* @param int $level The debug level of this message; see DEBUG_* constants
|
| 272 |
+
*
|
| 273 |
+
* @see SMTP::$Debugoutput
|
| 274 |
+
* @see SMTP::$do_debug
|
| 275 |
+
*/
|
| 276 |
+
protected function edebug($str, $level = 0)
|
| 277 |
+
{
|
| 278 |
+
if ($level > $this->do_debug) {
|
| 279 |
+
return;
|
| 280 |
+
}
|
| 281 |
+
//Is this a PSR-3 logger?
|
| 282 |
+
if ($this->Debugoutput instanceof \Psr\Log\LoggerInterface) {
|
| 283 |
+
//Remove trailing line breaks potentially added by calls to SMTP::client_send()
|
| 284 |
+
$this->Debugoutput->debug(rtrim($str, "\r\n"));
|
| 285 |
+
|
| 286 |
+
return;
|
| 287 |
+
}
|
| 288 |
+
//Avoid clash with built-in function names
|
| 289 |
+
if (is_callable($this->Debugoutput) && !in_array($this->Debugoutput, ['error_log', 'html', 'echo'])) {
|
| 290 |
+
call_user_func($this->Debugoutput, $str, $level);
|
| 291 |
+
|
| 292 |
+
return;
|
| 293 |
+
}
|
| 294 |
+
switch ($this->Debugoutput) {
|
| 295 |
+
case 'error_log':
|
| 296 |
+
//Don't output, just log
|
| 297 |
+
/** @noinspection ForgottenDebugOutputInspection */
|
| 298 |
+
error_log($str);
|
| 299 |
+
break;
|
| 300 |
+
case 'html':
|
| 301 |
+
//Cleans up output a bit for a better looking, HTML-safe output
|
| 302 |
+
echo gmdate('Y-m-d H:i:s'), ' ', htmlentities(
|
| 303 |
+
preg_replace('/[\r\n]+/', '', $str),
|
| 304 |
+
ENT_QUOTES,
|
| 305 |
+
'UTF-8'
|
| 306 |
+
), "<br>\n";
|
| 307 |
+
break;
|
| 308 |
+
case 'echo':
|
| 309 |
+
default:
|
| 310 |
+
//Normalize line breaks
|
| 311 |
+
$str = preg_replace('/\r\n|\r/m', "\n", $str);
|
| 312 |
+
echo gmdate('Y-m-d H:i:s'),
|
| 313 |
+
"\t",
|
| 314 |
+
//Trim trailing space
|
| 315 |
+
trim(
|
| 316 |
+
//Indent for readability, except for trailing break
|
| 317 |
+
str_replace(
|
| 318 |
+
"\n",
|
| 319 |
+
"\n \t ",
|
| 320 |
+
trim($str)
|
| 321 |
+
)
|
| 322 |
+
),
|
| 323 |
+
"\n";
|
| 324 |
+
}
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
/**
|
| 328 |
+
* Connect to an SMTP server.
|
| 329 |
+
*
|
| 330 |
+
* @param string $host SMTP server IP or host name
|
| 331 |
+
* @param int $port The port number to connect to
|
| 332 |
+
* @param int $timeout How long to wait for the connection to open
|
| 333 |
+
* @param array $options An array of options for stream_context_create()
|
| 334 |
+
*
|
| 335 |
+
* @return bool
|
| 336 |
+
*/
|
| 337 |
+
public function connect($host, $port = null, $timeout = 30, $options = [])
|
| 338 |
+
{
|
| 339 |
+
//Clear errors to avoid confusion
|
| 340 |
+
$this->setError('');
|
| 341 |
+
//Make sure we are __not__ connected
|
| 342 |
+
if ($this->connected()) {
|
| 343 |
+
//Already connected, generate error
|
| 344 |
+
$this->setError('Already connected to a server');
|
| 345 |
+
|
| 346 |
+
return false;
|
| 347 |
+
}
|
| 348 |
+
if (empty($port)) {
|
| 349 |
+
$port = self::DEFAULT_PORT;
|
| 350 |
+
}
|
| 351 |
+
//Connect to the SMTP server
|
| 352 |
+
$this->edebug(
|
| 353 |
+
"Connection: opening to $host:$port, timeout=$timeout, options=" .
|
| 354 |
+
(count($options) > 0 ? var_export($options, true) : 'array()'),
|
| 355 |
+
self::DEBUG_CONNECTION
|
| 356 |
+
);
|
| 357 |
+
|
| 358 |
+
$this->smtp_conn = $this->getSMTPConnection($host, $port, $timeout, $options);
|
| 359 |
+
|
| 360 |
+
if ($this->smtp_conn === false) {
|
| 361 |
+
//Error info already set inside `getSMTPConnection()`
|
| 362 |
+
return false;
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
+
$this->edebug('Connection: opened', self::DEBUG_CONNECTION);
|
| 366 |
+
|
| 367 |
+
//Get any announcement
|
| 368 |
+
$this->last_reply = $this->get_lines();
|
| 369 |
+
$this->edebug('SERVER -> CLIENT: ' . $this->last_reply, self::DEBUG_SERVER);
|
| 370 |
+
$responseCode = (int)substr($this->last_reply, 0, 3);
|
| 371 |
+
if ($responseCode === 220) {
|
| 372 |
+
return true;
|
| 373 |
+
}
|
| 374 |
+
//Anything other than a 220 response means something went wrong
|
| 375 |
+
//RFC 5321 says the server will wait for us to send a QUIT in response to a 554 error
|
| 376 |
+
//https://www.rfc-editor.org/rfc/rfc5321#section-3.1
|
| 377 |
+
if ($responseCode === 554) {
|
| 378 |
+
$this->quit();
|
| 379 |
+
}
|
| 380 |
+
//This will handle 421 responses which may not wait for a QUIT (e.g. if the server is being shut down)
|
| 381 |
+
$this->edebug('Connection: closing due to error', self::DEBUG_CONNECTION);
|
| 382 |
+
$this->close();
|
| 383 |
+
return false;
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
/**
|
| 387 |
+
* Create connection to the SMTP server.
|
| 388 |
+
*
|
| 389 |
+
* @param string $host SMTP server IP or host name
|
| 390 |
+
* @param int $port The port number to connect to
|
| 391 |
+
* @param int $timeout How long to wait for the connection to open
|
| 392 |
+
* @param array $options An array of options for stream_context_create()
|
| 393 |
+
*
|
| 394 |
+
* @return false|resource
|
| 395 |
+
*/
|
| 396 |
+
protected function getSMTPConnection($host, $port = null, $timeout = 30, $options = [])
|
| 397 |
+
{
|
| 398 |
+
static $streamok;
|
| 399 |
+
//This is enabled by default since 5.0.0 but some providers disable it
|
| 400 |
+
//Check this once and cache the result
|
| 401 |
+
if (null === $streamok) {
|
| 402 |
+
$streamok = function_exists('stream_socket_client');
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
$errno = 0;
|
| 406 |
+
$errstr = '';
|
| 407 |
+
if ($streamok) {
|
| 408 |
+
$socket_context = stream_context_create($options);
|
| 409 |
+
set_error_handler(function () {
|
| 410 |
+
call_user_func_array([$this, 'errorHandler'], func_get_args());
|
| 411 |
+
});
|
| 412 |
+
$connection = stream_socket_client(
|
| 413 |
+
$host . ':' . $port,
|
| 414 |
+
$errno,
|
| 415 |
+
$errstr,
|
| 416 |
+
$timeout,
|
| 417 |
+
STREAM_CLIENT_CONNECT,
|
| 418 |
+
$socket_context
|
| 419 |
+
);
|
| 420 |
+
} else {
|
| 421 |
+
//Fall back to fsockopen which should work in more places, but is missing some features
|
| 422 |
+
$this->edebug(
|
| 423 |
+
'Connection: stream_socket_client not available, falling back to fsockopen',
|
| 424 |
+
self::DEBUG_CONNECTION
|
| 425 |
+
);
|
| 426 |
+
set_error_handler(function () {
|
| 427 |
+
call_user_func_array([$this, 'errorHandler'], func_get_args());
|
| 428 |
+
});
|
| 429 |
+
$connection = fsockopen(
|
| 430 |
+
$host,
|
| 431 |
+
$port,
|
| 432 |
+
$errno,
|
| 433 |
+
$errstr,
|
| 434 |
+
$timeout
|
| 435 |
+
);
|
| 436 |
+
}
|
| 437 |
+
restore_error_handler();
|
| 438 |
+
|
| 439 |
+
//Verify we connected properly
|
| 440 |
+
if (!is_resource($connection)) {
|
| 441 |
+
$this->setError(
|
| 442 |
+
'Failed to connect to server',
|
| 443 |
+
'',
|
| 444 |
+
(string) $errno,
|
| 445 |
+
$errstr
|
| 446 |
+
);
|
| 447 |
+
$this->edebug(
|
| 448 |
+
'SMTP ERROR: ' . $this->error['error']
|
| 449 |
+
. ": $errstr ($errno)",
|
| 450 |
+
self::DEBUG_CLIENT
|
| 451 |
+
);
|
| 452 |
+
|
| 453 |
+
return false;
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
//SMTP server can take longer to respond, give longer timeout for first read
|
| 457 |
+
//Windows does not have support for this timeout function
|
| 458 |
+
if (strpos(PHP_OS, 'WIN') !== 0) {
|
| 459 |
+
$max = (int)ini_get('max_execution_time');
|
| 460 |
+
//Don't bother if unlimited, or if set_time_limit is disabled
|
| 461 |
+
if (0 !== $max && $timeout > $max && strpos(ini_get('disable_functions'), 'set_time_limit') === false) {
|
| 462 |
+
@set_time_limit($timeout);
|
| 463 |
+
}
|
| 464 |
+
stream_set_timeout($connection, $timeout, 0);
|
| 465 |
+
}
|
| 466 |
+
|
| 467 |
+
return $connection;
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
/**
|
| 471 |
+
* Initiate a TLS (encrypted) session.
|
| 472 |
+
*
|
| 473 |
+
* @return bool
|
| 474 |
+
*/
|
| 475 |
+
public function startTLS()
|
| 476 |
+
{
|
| 477 |
+
if (!$this->sendCommand('STARTTLS', 'STARTTLS', 220)) {
|
| 478 |
+
return false;
|
| 479 |
+
}
|
| 480 |
+
|
| 481 |
+
//Allow the best TLS version(s) we can
|
| 482 |
+
$crypto_method = STREAM_CRYPTO_METHOD_TLS_CLIENT;
|
| 483 |
+
|
| 484 |
+
//PHP 5.6.7 dropped inclusion of TLS 1.1 and 1.2 in STREAM_CRYPTO_METHOD_TLS_CLIENT
|
| 485 |
+
//so add them back in manually if we can
|
| 486 |
+
if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) {
|
| 487 |
+
$crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
|
| 488 |
+
$crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;
|
| 489 |
+
}
|
| 490 |
+
|
| 491 |
+
//Begin encrypted connection
|
| 492 |
+
set_error_handler(function () {
|
| 493 |
+
call_user_func_array([$this, 'errorHandler'], func_get_args());
|
| 494 |
+
});
|
| 495 |
+
$crypto_ok = stream_socket_enable_crypto(
|
| 496 |
+
$this->smtp_conn,
|
| 497 |
+
true,
|
| 498 |
+
$crypto_method
|
| 499 |
+
);
|
| 500 |
+
restore_error_handler();
|
| 501 |
+
|
| 502 |
+
return (bool) $crypto_ok;
|
| 503 |
+
}
|
| 504 |
+
|
| 505 |
+
/**
|
| 506 |
+
* Perform SMTP authentication.
|
| 507 |
+
* Must be run after hello().
|
| 508 |
+
*
|
| 509 |
+
* @see hello()
|
| 510 |
+
*
|
| 511 |
+
* @param string $username The user name
|
| 512 |
+
* @param string $password The password
|
| 513 |
+
* @param string $authtype The auth type (CRAM-MD5, PLAIN, LOGIN, XOAUTH2)
|
| 514 |
+
* @param OAuthTokenProvider $OAuth An optional OAuthTokenProvider instance for XOAUTH2 authentication
|
| 515 |
+
*
|
| 516 |
+
* @return bool True if successfully authenticated
|
| 517 |
+
*/
|
| 518 |
+
public function authenticate(
|
| 519 |
+
$username,
|
| 520 |
+
$password,
|
| 521 |
+
$authtype = null,
|
| 522 |
+
$OAuth = null
|
| 523 |
+
) {
|
| 524 |
+
if (!$this->server_caps) {
|
| 525 |
+
$this->setError('Authentication is not allowed before HELO/EHLO');
|
| 526 |
+
|
| 527 |
+
return false;
|
| 528 |
+
}
|
| 529 |
+
|
| 530 |
+
if (array_key_exists('EHLO', $this->server_caps)) {
|
| 531 |
+
//SMTP extensions are available; try to find a proper authentication method
|
| 532 |
+
if (!array_key_exists('AUTH', $this->server_caps)) {
|
| 533 |
+
$this->setError('Authentication is not allowed at this stage');
|
| 534 |
+
//'at this stage' means that auth may be allowed after the stage changes
|
| 535 |
+
//e.g. after STARTTLS
|
| 536 |
+
|
| 537 |
+
return false;
|
| 538 |
+
}
|
| 539 |
+
|
| 540 |
+
$this->edebug('Auth method requested: ' . ($authtype ?: 'UNSPECIFIED'), self::DEBUG_LOWLEVEL);
|
| 541 |
+
$this->edebug(
|
| 542 |
+
'Auth methods available on the server: ' . implode(',', $this->server_caps['AUTH']),
|
| 543 |
+
self::DEBUG_LOWLEVEL
|
| 544 |
+
);
|
| 545 |
+
|
| 546 |
+
//If we have requested a specific auth type, check the server supports it before trying others
|
| 547 |
+
if (null !== $authtype && !in_array($authtype, $this->server_caps['AUTH'], true)) {
|
| 548 |
+
$this->edebug('Requested auth method not available: ' . $authtype, self::DEBUG_LOWLEVEL);
|
| 549 |
+
$authtype = null;
|
| 550 |
+
}
|
| 551 |
+
|
| 552 |
+
if (empty($authtype)) {
|
| 553 |
+
//If no auth mechanism is specified, attempt to use these, in this order
|
| 554 |
+
//Try CRAM-MD5 first as it's more secure than the others
|
| 555 |
+
foreach (['CRAM-MD5', 'LOGIN', 'PLAIN', 'XOAUTH2'] as $method) {
|
| 556 |
+
if (in_array($method, $this->server_caps['AUTH'], true)) {
|
| 557 |
+
$authtype = $method;
|
| 558 |
+
break;
|
| 559 |
+
}
|
| 560 |
+
}
|
| 561 |
+
if (empty($authtype)) {
|
| 562 |
+
$this->setError('No supported authentication methods found');
|
| 563 |
+
|
| 564 |
+
return false;
|
| 565 |
+
}
|
| 566 |
+
$this->edebug('Auth method selected: ' . $authtype, self::DEBUG_LOWLEVEL);
|
| 567 |
+
}
|
| 568 |
+
|
| 569 |
+
if (!in_array($authtype, $this->server_caps['AUTH'], true)) {
|
| 570 |
+
$this->setError("The requested authentication method \"$authtype\" is not supported by the server");
|
| 571 |
+
|
| 572 |
+
return false;
|
| 573 |
+
}
|
| 574 |
+
} elseif (empty($authtype)) {
|
| 575 |
+
$authtype = 'LOGIN';
|
| 576 |
+
}
|
| 577 |
+
switch ($authtype) {
|
| 578 |
+
case 'PLAIN':
|
| 579 |
+
//Start authentication
|
| 580 |
+
if (!$this->sendCommand('AUTH', 'AUTH PLAIN', 334)) {
|
| 581 |
+
return false;
|
| 582 |
+
}
|
| 583 |
+
//Send encoded username and password
|
| 584 |
+
if (
|
| 585 |
+
//Format from https://www.rfc-editor.org/rfc/rfc4616#section-2
|
| 586 |
+
//We skip the first field (it's forgery), so the string starts with a null byte
|
| 587 |
+
!$this->sendCommand(
|
| 588 |
+
'User & Password',
|
| 589 |
+
base64_encode("\0" . $username . "\0" . $password),
|
| 590 |
+
235
|
| 591 |
+
)
|
| 592 |
+
) {
|
| 593 |
+
return false;
|
| 594 |
+
}
|
| 595 |
+
break;
|
| 596 |
+
case 'LOGIN':
|
| 597 |
+
//Start authentication
|
| 598 |
+
if (!$this->sendCommand('AUTH', 'AUTH LOGIN', 334)) {
|
| 599 |
+
return false;
|
| 600 |
+
}
|
| 601 |
+
if (!$this->sendCommand('Username', base64_encode($username), 334)) {
|
| 602 |
+
return false;
|
| 603 |
+
}
|
| 604 |
+
if (!$this->sendCommand('Password', base64_encode($password), 235)) {
|
| 605 |
+
return false;
|
| 606 |
+
}
|
| 607 |
+
break;
|
| 608 |
+
case 'CRAM-MD5':
|
| 609 |
+
//Start authentication
|
| 610 |
+
if (!$this->sendCommand('AUTH CRAM-MD5', 'AUTH CRAM-MD5', 334)) {
|
| 611 |
+
return false;
|
| 612 |
+
}
|
| 613 |
+
//Get the challenge
|
| 614 |
+
$challenge = base64_decode(substr($this->last_reply, 4));
|
| 615 |
+
|
| 616 |
+
//Build the response
|
| 617 |
+
$response = $username . ' ' . $this->hmac($challenge, $password);
|
| 618 |
+
|
| 619 |
+
//send encoded credentials
|
| 620 |
+
return $this->sendCommand('Username', base64_encode($response), 235);
|
| 621 |
+
case 'XOAUTH2':
|
| 622 |
+
//The OAuth instance must be set up prior to requesting auth.
|
| 623 |
+
if (null === $OAuth) {
|
| 624 |
+
return false;
|
| 625 |
+
}
|
| 626 |
+
$oauth = $OAuth->getOauth64();
|
| 627 |
+
|
| 628 |
+
//Start authentication
|
| 629 |
+
if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2 ' . $oauth, 235)) {
|
| 630 |
+
return false;
|
| 631 |
+
}
|
| 632 |
+
break;
|
| 633 |
+
default:
|
| 634 |
+
$this->setError("Authentication method \"$authtype\" is not supported");
|
| 635 |
+
|
| 636 |
+
return false;
|
| 637 |
+
}
|
| 638 |
+
|
| 639 |
+
return true;
|
| 640 |
+
}
|
| 641 |
+
|
| 642 |
+
/**
|
| 643 |
+
* Calculate an MD5 HMAC hash.
|
| 644 |
+
* Works like hash_hmac('md5', $data, $key)
|
| 645 |
+
* in case that function is not available.
|
| 646 |
+
*
|
| 647 |
+
* @param string $data The data to hash
|
| 648 |
+
* @param string $key The key to hash with
|
| 649 |
+
*
|
| 650 |
+
* @return string
|
| 651 |
+
*/
|
| 652 |
+
protected function hmac($data, $key)
|
| 653 |
+
{
|
| 654 |
+
if (function_exists('hash_hmac')) {
|
| 655 |
+
return hash_hmac('md5', $data, $key);
|
| 656 |
+
}
|
| 657 |
+
|
| 658 |
+
//The following borrowed from
|
| 659 |
+
//https://www.php.net/manual/en/function.mhash.php#27225
|
| 660 |
+
|
| 661 |
+
//RFC 2104 HMAC implementation for php.
|
| 662 |
+
//Creates an md5 HMAC.
|
| 663 |
+
//Eliminates the need to install mhash to compute a HMAC
|
| 664 |
+
//by Lance Rushing
|
| 665 |
+
|
| 666 |
+
$bytelen = 64; //byte length for md5
|
| 667 |
+
if (strlen($key) > $bytelen) {
|
| 668 |
+
$key = pack('H*', md5($key));
|
| 669 |
+
}
|
| 670 |
+
$key = str_pad($key, $bytelen, chr(0x00));
|
| 671 |
+
$ipad = str_pad('', $bytelen, chr(0x36));
|
| 672 |
+
$opad = str_pad('', $bytelen, chr(0x5c));
|
| 673 |
+
$k_ipad = $key ^ $ipad;
|
| 674 |
+
$k_opad = $key ^ $opad;
|
| 675 |
+
|
| 676 |
+
return md5($k_opad . pack('H*', md5($k_ipad . $data)));
|
| 677 |
+
}
|
| 678 |
+
|
| 679 |
+
/**
|
| 680 |
+
* Check connection state.
|
| 681 |
+
*
|
| 682 |
+
* @return bool True if connected
|
| 683 |
+
*/
|
| 684 |
+
public function connected()
|
| 685 |
+
{
|
| 686 |
+
if (is_resource($this->smtp_conn)) {
|
| 687 |
+
$sock_status = stream_get_meta_data($this->smtp_conn);
|
| 688 |
+
if ($sock_status['eof']) {
|
| 689 |
+
//The socket is valid but we are not connected
|
| 690 |
+
$this->edebug(
|
| 691 |
+
'SMTP NOTICE: EOF caught while checking if connected',
|
| 692 |
+
self::DEBUG_CLIENT
|
| 693 |
+
);
|
| 694 |
+
$this->close();
|
| 695 |
+
|
| 696 |
+
return false;
|
| 697 |
+
}
|
| 698 |
+
|
| 699 |
+
return true; //everything looks good
|
| 700 |
+
}
|
| 701 |
+
|
| 702 |
+
return false;
|
| 703 |
+
}
|
| 704 |
+
|
| 705 |
+
/**
|
| 706 |
+
* Close the socket and clean up the state of the class.
|
| 707 |
+
* Don't use this function without first trying to use QUIT.
|
| 708 |
+
*
|
| 709 |
+
* @see quit()
|
| 710 |
+
*/
|
| 711 |
+
public function close()
|
| 712 |
+
{
|
| 713 |
+
$this->server_caps = null;
|
| 714 |
+
$this->helo_rply = null;
|
| 715 |
+
if (is_resource($this->smtp_conn)) {
|
| 716 |
+
//Close the connection and cleanup
|
| 717 |
+
fclose($this->smtp_conn);
|
| 718 |
+
$this->smtp_conn = null; //Makes for cleaner serialization
|
| 719 |
+
$this->edebug('Connection: closed', self::DEBUG_CONNECTION);
|
| 720 |
+
}
|
| 721 |
+
}
|
| 722 |
+
|
| 723 |
+
/**
|
| 724 |
+
* Send an SMTP DATA command.
|
| 725 |
+
* Issues a data command and sends the msg_data to the server,
|
| 726 |
+
* finalizing the mail transaction. $msg_data is the message
|
| 727 |
+
* that is to be sent with the headers. Each header needs to be
|
| 728 |
+
* on a single line followed by a <CRLF> with the message headers
|
| 729 |
+
* and the message body being separated by an additional <CRLF>.
|
| 730 |
+
* Implements RFC 821: DATA <CRLF>.
|
| 731 |
+
*
|
| 732 |
+
* @param string $msg_data Message data to send
|
| 733 |
+
*
|
| 734 |
+
* @return bool
|
| 735 |
+
*/
|
| 736 |
+
public function data($msg_data)
|
| 737 |
+
{
|
| 738 |
+
//This will use the standard timelimit
|
| 739 |
+
if (!$this->sendCommand('DATA', 'DATA', 354)) {
|
| 740 |
+
return false;
|
| 741 |
+
}
|
| 742 |
+
|
| 743 |
+
/* The server is ready to accept data!
|
| 744 |
+
* According to rfc821 we should not send more than 1000 characters on a single line (including the LE)
|
| 745 |
+
* so we will break the data up into lines by \r and/or \n then if needed we will break each of those into
|
| 746 |
+
* smaller lines to fit within the limit.
|
| 747 |
+
* We will also look for lines that start with a '.' and prepend an additional '.'.
|
| 748 |
+
* NOTE: this does not count towards line-length limit.
|
| 749 |
+
*/
|
| 750 |
+
|
| 751 |
+
//Normalize line breaks before exploding
|
| 752 |
+
$lines = explode("\n", str_replace(["\r\n", "\r"], "\n", $msg_data));
|
| 753 |
+
|
| 754 |
+
/* To distinguish between a complete RFC822 message and a plain message body, we check if the first field
|
| 755 |
+
* of the first line (':' separated) does not contain a space then it _should_ be a header, and we will
|
| 756 |
+
* process all lines before a blank line as headers.
|
| 757 |
+
*/
|
| 758 |
+
|
| 759 |
+
$field = substr($lines[0], 0, strpos($lines[0], ':'));
|
| 760 |
+
$in_headers = false;
|
| 761 |
+
if (!empty($field) && strpos($field, ' ') === false) {
|
| 762 |
+
$in_headers = true;
|
| 763 |
+
}
|
| 764 |
+
|
| 765 |
+
foreach ($lines as $line) {
|
| 766 |
+
$lines_out = [];
|
| 767 |
+
if ($in_headers && $line === '') {
|
| 768 |
+
$in_headers = false;
|
| 769 |
+
}
|
| 770 |
+
//Break this line up into several smaller lines if it's too long
|
| 771 |
+
//Micro-optimisation: isset($str[$len]) is faster than (strlen($str) > $len),
|
| 772 |
+
while (isset($line[self::MAX_LINE_LENGTH])) {
|
| 773 |
+
//Working backwards, try to find a space within the last MAX_LINE_LENGTH chars of the line to break on
|
| 774 |
+
//so as to avoid breaking in the middle of a word
|
| 775 |
+
$pos = strrpos(substr($line, 0, self::MAX_LINE_LENGTH), ' ');
|
| 776 |
+
//Deliberately matches both false and 0
|
| 777 |
+
if (!$pos) {
|
| 778 |
+
//No nice break found, add a hard break
|
| 779 |
+
$pos = self::MAX_LINE_LENGTH - 1;
|
| 780 |
+
$lines_out[] = substr($line, 0, $pos);
|
| 781 |
+
$line = substr($line, $pos);
|
| 782 |
+
} else {
|
| 783 |
+
//Break at the found point
|
| 784 |
+
$lines_out[] = substr($line, 0, $pos);
|
| 785 |
+
//Move along by the amount we dealt with
|
| 786 |
+
$line = substr($line, $pos + 1);
|
| 787 |
+
}
|
| 788 |
+
//If processing headers add a LWSP-char to the front of new line RFC822 section 3.1.1
|
| 789 |
+
if ($in_headers) {
|
| 790 |
+
$line = "\t" . $line;
|
| 791 |
+
}
|
| 792 |
+
}
|
| 793 |
+
$lines_out[] = $line;
|
| 794 |
+
|
| 795 |
+
//Send the lines to the server
|
| 796 |
+
foreach ($lines_out as $line_out) {
|
| 797 |
+
//Dot-stuffing as per RFC5321 section 4.5.2
|
| 798 |
+
//https://www.rfc-editor.org/rfc/rfc5321#section-4.5.2
|
| 799 |
+
if (!empty($line_out) && $line_out[0] === '.') {
|
| 800 |
+
$line_out = '.' . $line_out;
|
| 801 |
+
}
|
| 802 |
+
$this->client_send($line_out . static::LE, 'DATA');
|
| 803 |
+
}
|
| 804 |
+
}
|
| 805 |
+
|
| 806 |
+
//Message data has been sent, complete the command
|
| 807 |
+
//Increase timelimit for end of DATA command
|
| 808 |
+
$savetimelimit = $this->Timelimit;
|
| 809 |
+
$this->Timelimit *= 2;
|
| 810 |
+
$result = $this->sendCommand('DATA END', '.', 250);
|
| 811 |
+
$this->recordLastTransactionID();
|
| 812 |
+
//Restore timelimit
|
| 813 |
+
$this->Timelimit = $savetimelimit;
|
| 814 |
+
|
| 815 |
+
return $result;
|
| 816 |
+
}
|
| 817 |
+
|
| 818 |
+
/**
|
| 819 |
+
* Send an SMTP HELO or EHLO command.
|
| 820 |
+
* Used to identify the sending server to the receiving server.
|
| 821 |
+
* This makes sure that client and server are in a known state.
|
| 822 |
+
* Implements RFC 821: HELO <SP> <domain> <CRLF>
|
| 823 |
+
* and RFC 2821 EHLO.
|
| 824 |
+
*
|
| 825 |
+
* @param string $host The host name or IP to connect to
|
| 826 |
+
*
|
| 827 |
+
* @return bool
|
| 828 |
+
*/
|
| 829 |
+
public function hello($host = '')
|
| 830 |
+
{
|
| 831 |
+
//Try extended hello first (RFC 2821)
|
| 832 |
+
if ($this->sendHello('EHLO', $host)) {
|
| 833 |
+
return true;
|
| 834 |
+
}
|
| 835 |
+
|
| 836 |
+
//Some servers shut down the SMTP service here (RFC 5321)
|
| 837 |
+
if (substr($this->helo_rply, 0, 3) == '421') {
|
| 838 |
+
return false;
|
| 839 |
+
}
|
| 840 |
+
|
| 841 |
+
return $this->sendHello('HELO', $host);
|
| 842 |
+
}
|
| 843 |
+
|
| 844 |
+
/**
|
| 845 |
+
* Send an SMTP HELO or EHLO command.
|
| 846 |
+
* Low-level implementation used by hello().
|
| 847 |
+
*
|
| 848 |
+
* @param string $hello The HELO string
|
| 849 |
+
* @param string $host The hostname to say we are
|
| 850 |
+
*
|
| 851 |
+
* @return bool
|
| 852 |
+
*
|
| 853 |
+
* @see hello()
|
| 854 |
+
*/
|
| 855 |
+
protected function sendHello($hello, $host)
|
| 856 |
+
{
|
| 857 |
+
$noerror = $this->sendCommand($hello, $hello . ' ' . $host, 250);
|
| 858 |
+
$this->helo_rply = $this->last_reply;
|
| 859 |
+
if ($noerror) {
|
| 860 |
+
$this->parseHelloFields($hello);
|
| 861 |
+
} else {
|
| 862 |
+
$this->server_caps = null;
|
| 863 |
+
}
|
| 864 |
+
|
| 865 |
+
return $noerror;
|
| 866 |
+
}
|
| 867 |
+
|
| 868 |
+
/**
|
| 869 |
+
* Parse a reply to HELO/EHLO command to discover server extensions.
|
| 870 |
+
* In case of HELO, the only parameter that can be discovered is a server name.
|
| 871 |
+
*
|
| 872 |
+
* @param string $type `HELO` or `EHLO`
|
| 873 |
+
*/
|
| 874 |
+
protected function parseHelloFields($type)
|
| 875 |
+
{
|
| 876 |
+
$this->server_caps = [];
|
| 877 |
+
$lines = explode("\n", $this->helo_rply);
|
| 878 |
+
|
| 879 |
+
foreach ($lines as $n => $s) {
|
| 880 |
+
//First 4 chars contain response code followed by - or space
|
| 881 |
+
$s = trim(substr($s, 4));
|
| 882 |
+
if (empty($s)) {
|
| 883 |
+
continue;
|
| 884 |
+
}
|
| 885 |
+
$fields = explode(' ', $s);
|
| 886 |
+
if (!empty($fields)) {
|
| 887 |
+
if (!$n) {
|
| 888 |
+
$name = $type;
|
| 889 |
+
$fields = $fields[0];
|
| 890 |
+
} else {
|
| 891 |
+
$name = array_shift($fields);
|
| 892 |
+
switch ($name) {
|
| 893 |
+
case 'SIZE':
|
| 894 |
+
$fields = ($fields ? $fields[0] : 0);
|
| 895 |
+
break;
|
| 896 |
+
case 'AUTH':
|
| 897 |
+
if (!is_array($fields)) {
|
| 898 |
+
$fields = [];
|
| 899 |
+
}
|
| 900 |
+
break;
|
| 901 |
+
default:
|
| 902 |
+
$fields = true;
|
| 903 |
+
}
|
| 904 |
+
}
|
| 905 |
+
$this->server_caps[$name] = $fields;
|
| 906 |
+
}
|
| 907 |
+
}
|
| 908 |
+
}
|
| 909 |
+
|
| 910 |
+
/**
|
| 911 |
+
* Send an SMTP MAIL command.
|
| 912 |
+
* Starts a mail transaction from the email address specified in
|
| 913 |
+
* $from. Returns true if successful or false otherwise. If True
|
| 914 |
+
* the mail transaction is started and then one or more recipient
|
| 915 |
+
* commands may be called followed by a data command.
|
| 916 |
+
* Implements RFC 821: MAIL <SP> FROM:<reverse-path> <CRLF>.
|
| 917 |
+
*
|
| 918 |
+
* @param string $from Source address of this message
|
| 919 |
+
*
|
| 920 |
+
* @return bool
|
| 921 |
+
*/
|
| 922 |
+
public function mail($from)
|
| 923 |
+
{
|
| 924 |
+
$useVerp = ($this->do_verp ? ' XVERP' : '');
|
| 925 |
+
|
| 926 |
+
return $this->sendCommand(
|
| 927 |
+
'MAIL FROM',
|
| 928 |
+
'MAIL FROM:<' . $from . '>' . $useVerp,
|
| 929 |
+
250
|
| 930 |
+
);
|
| 931 |
+
}
|
| 932 |
+
|
| 933 |
+
/**
|
| 934 |
+
* Send an SMTP QUIT command.
|
| 935 |
+
* Closes the socket if there is no error or the $close_on_error argument is true.
|
| 936 |
+
* Implements from RFC 821: QUIT <CRLF>.
|
| 937 |
+
*
|
| 938 |
+
* @param bool $close_on_error Should the connection close if an error occurs?
|
| 939 |
+
*
|
| 940 |
+
* @return bool
|
| 941 |
+
*/
|
| 942 |
+
public function quit($close_on_error = true)
|
| 943 |
+
{
|
| 944 |
+
$noerror = $this->sendCommand('QUIT', 'QUIT', 221);
|
| 945 |
+
$err = $this->error; //Save any error
|
| 946 |
+
if ($noerror || $close_on_error) {
|
| 947 |
+
$this->close();
|
| 948 |
+
$this->error = $err; //Restore any error from the quit command
|
| 949 |
+
}
|
| 950 |
+
|
| 951 |
+
return $noerror;
|
| 952 |
+
}
|
| 953 |
+
|
| 954 |
+
/**
|
| 955 |
+
* Send an SMTP RCPT command.
|
| 956 |
+
* Sets the TO argument to $toaddr.
|
| 957 |
+
* Returns true if the recipient was accepted false if it was rejected.
|
| 958 |
+
* Implements from RFC 821: RCPT <SP> TO:<forward-path> <CRLF>.
|
| 959 |
+
*
|
| 960 |
+
* @param string $address The address the message is being sent to
|
| 961 |
+
* @param string $dsn Comma separated list of DSN notifications. NEVER, SUCCESS, FAILURE
|
| 962 |
+
* or DELAY. If you specify NEVER all other notifications are ignored.
|
| 963 |
+
*
|
| 964 |
+
* @return bool
|
| 965 |
+
*/
|
| 966 |
+
public function recipient($address, $dsn = '')
|
| 967 |
+
{
|
| 968 |
+
if (empty($dsn)) {
|
| 969 |
+
$rcpt = 'RCPT TO:<' . $address . '>';
|
| 970 |
+
} else {
|
| 971 |
+
$dsn = strtoupper($dsn);
|
| 972 |
+
$notify = [];
|
| 973 |
+
|
| 974 |
+
if (strpos($dsn, 'NEVER') !== false) {
|
| 975 |
+
$notify[] = 'NEVER';
|
| 976 |
+
} else {
|
| 977 |
+
foreach (['SUCCESS', 'FAILURE', 'DELAY'] as $value) {
|
| 978 |
+
if (strpos($dsn, $value) !== false) {
|
| 979 |
+
$notify[] = $value;
|
| 980 |
+
}
|
| 981 |
+
}
|
| 982 |
+
}
|
| 983 |
+
|
| 984 |
+
$rcpt = 'RCPT TO:<' . $address . '> NOTIFY=' . implode(',', $notify);
|
| 985 |
+
}
|
| 986 |
+
|
| 987 |
+
return $this->sendCommand(
|
| 988 |
+
'RCPT TO',
|
| 989 |
+
$rcpt,
|
| 990 |
+
[250, 251]
|
| 991 |
+
);
|
| 992 |
+
}
|
| 993 |
+
|
| 994 |
+
/**
|
| 995 |
+
* Send SMTP XCLIENT command to server and check its return code.
|
| 996 |
+
*
|
| 997 |
+
* @return bool True on success
|
| 998 |
+
*/
|
| 999 |
+
public function xclient(array $vars)
|
| 1000 |
+
{
|
| 1001 |
+
$xclient_options = "";
|
| 1002 |
+
foreach ($vars as $key => $value) {
|
| 1003 |
+
if (in_array($key, SMTP::$xclient_allowed_attributes)) {
|
| 1004 |
+
$xclient_options .= " {$key}={$value}";
|
| 1005 |
+
}
|
| 1006 |
+
}
|
| 1007 |
+
if (!$xclient_options) {
|
| 1008 |
+
return true;
|
| 1009 |
+
}
|
| 1010 |
+
return $this->sendCommand('XCLIENT', 'XCLIENT' . $xclient_options, 250);
|
| 1011 |
+
}
|
| 1012 |
+
|
| 1013 |
+
/**
|
| 1014 |
+
* Send an SMTP RSET command.
|
| 1015 |
+
* Abort any transaction that is currently in progress.
|
| 1016 |
+
* Implements RFC 821: RSET <CRLF>.
|
| 1017 |
+
*
|
| 1018 |
+
* @return bool True on success
|
| 1019 |
+
*/
|
| 1020 |
+
public function reset()
|
| 1021 |
+
{
|
| 1022 |
+
return $this->sendCommand('RSET', 'RSET', 250);
|
| 1023 |
+
}
|
| 1024 |
+
|
| 1025 |
+
/**
|
| 1026 |
+
* Send a command to an SMTP server and check its return code.
|
| 1027 |
+
*
|
| 1028 |
+
* @param string $command The command name - not sent to the server
|
| 1029 |
+
* @param string $commandstring The actual command to send
|
| 1030 |
+
* @param int|array $expect One or more expected integer success codes
|
| 1031 |
+
*
|
| 1032 |
+
* @return bool True on success
|
| 1033 |
+
*/
|
| 1034 |
+
protected function sendCommand($command, $commandstring, $expect)
|
| 1035 |
+
{
|
| 1036 |
+
if (!$this->connected()) {
|
| 1037 |
+
$this->setError("Called $command without being connected");
|
| 1038 |
+
|
| 1039 |
+
return false;
|
| 1040 |
+
}
|
| 1041 |
+
//Reject line breaks in all commands
|
| 1042 |
+
if ((strpos($commandstring, "\n") !== false) || (strpos($commandstring, "\r") !== false)) {
|
| 1043 |
+
$this->setError("Command '$command' contained line breaks");
|
| 1044 |
+
|
| 1045 |
+
return false;
|
| 1046 |
+
}
|
| 1047 |
+
$this->client_send($commandstring . static::LE, $command);
|
| 1048 |
+
|
| 1049 |
+
$this->last_reply = $this->get_lines();
|
| 1050 |
+
//Fetch SMTP code and possible error code explanation
|
| 1051 |
+
$matches = [];
|
| 1052 |
+
if (preg_match('/^([\d]{3})[ -](?:([\d]\\.[\d]\\.[\d]{1,2}) )?/', $this->last_reply, $matches)) {
|
| 1053 |
+
$code = (int) $matches[1];
|
| 1054 |
+
$code_ex = (count($matches) > 2 ? $matches[2] : null);
|
| 1055 |
+
//Cut off error code from each response line
|
| 1056 |
+
$detail = preg_replace(
|
| 1057 |
+
"/{$code}[ -]" .
|
| 1058 |
+
($code_ex ? str_replace('.', '\\.', $code_ex) . ' ' : '') . '/m',
|
| 1059 |
+
'',
|
| 1060 |
+
$this->last_reply
|
| 1061 |
+
);
|
| 1062 |
+
} else {
|
| 1063 |
+
//Fall back to simple parsing if regex fails
|
| 1064 |
+
$code = (int) substr($this->last_reply, 0, 3);
|
| 1065 |
+
$code_ex = null;
|
| 1066 |
+
$detail = substr($this->last_reply, 4);
|
| 1067 |
+
}
|
| 1068 |
+
|
| 1069 |
+
$this->edebug('SERVER -> CLIENT: ' . $this->last_reply, self::DEBUG_SERVER);
|
| 1070 |
+
|
| 1071 |
+
if (!in_array($code, (array) $expect, true)) {
|
| 1072 |
+
$this->setError(
|
| 1073 |
+
"$command command failed",
|
| 1074 |
+
$detail,
|
| 1075 |
+
$code,
|
| 1076 |
+
$code_ex
|
| 1077 |
+
);
|
| 1078 |
+
$this->edebug(
|
| 1079 |
+
'SMTP ERROR: ' . $this->error['error'] . ': ' . $this->last_reply,
|
| 1080 |
+
self::DEBUG_CLIENT
|
| 1081 |
+
);
|
| 1082 |
+
|
| 1083 |
+
return false;
|
| 1084 |
+
}
|
| 1085 |
+
|
| 1086 |
+
//Don't clear the error store when using keepalive
|
| 1087 |
+
if ($command !== 'RSET') {
|
| 1088 |
+
$this->setError('');
|
| 1089 |
+
}
|
| 1090 |
+
|
| 1091 |
+
return true;
|
| 1092 |
+
}
|
| 1093 |
+
|
| 1094 |
+
/**
|
| 1095 |
+
* Send an SMTP SAML command.
|
| 1096 |
+
* Starts a mail transaction from the email address specified in $from.
|
| 1097 |
+
* Returns true if successful or false otherwise. If True
|
| 1098 |
+
* the mail transaction is started and then one or more recipient
|
| 1099 |
+
* commands may be called followed by a data command. This command
|
| 1100 |
+
* will send the message to the users terminal if they are logged
|
| 1101 |
+
* in and send them an email.
|
| 1102 |
+
* Implements RFC 821: SAML <SP> FROM:<reverse-path> <CRLF>.
|
| 1103 |
+
*
|
| 1104 |
+
* @param string $from The address the message is from
|
| 1105 |
+
*
|
| 1106 |
+
* @return bool
|
| 1107 |
+
*/
|
| 1108 |
+
public function sendAndMail($from)
|
| 1109 |
+
{
|
| 1110 |
+
return $this->sendCommand('SAML', "SAML FROM:$from", 250);
|
| 1111 |
+
}
|
| 1112 |
+
|
| 1113 |
+
/**
|
| 1114 |
+
* Send an SMTP VRFY command.
|
| 1115 |
+
*
|
| 1116 |
+
* @param string $name The name to verify
|
| 1117 |
+
*
|
| 1118 |
+
* @return bool
|
| 1119 |
+
*/
|
| 1120 |
+
public function verify($name)
|
| 1121 |
+
{
|
| 1122 |
+
return $this->sendCommand('VRFY', "VRFY $name", [250, 251]);
|
| 1123 |
+
}
|
| 1124 |
+
|
| 1125 |
+
/**
|
| 1126 |
+
* Send an SMTP NOOP command.
|
| 1127 |
+
* Used to keep keep-alives alive, doesn't actually do anything.
|
| 1128 |
+
*
|
| 1129 |
+
* @return bool
|
| 1130 |
+
*/
|
| 1131 |
+
public function noop()
|
| 1132 |
+
{
|
| 1133 |
+
return $this->sendCommand('NOOP', 'NOOP', 250);
|
| 1134 |
+
}
|
| 1135 |
+
|
| 1136 |
+
/**
|
| 1137 |
+
* Send an SMTP TURN command.
|
| 1138 |
+
* This is an optional command for SMTP that this class does not support.
|
| 1139 |
+
* This method is here to make the RFC821 Definition complete for this class
|
| 1140 |
+
* and _may_ be implemented in future.
|
| 1141 |
+
* Implements from RFC 821: TURN <CRLF>.
|
| 1142 |
+
*
|
| 1143 |
+
* @return bool
|
| 1144 |
+
*/
|
| 1145 |
+
public function turn()
|
| 1146 |
+
{
|
| 1147 |
+
$this->setError('The SMTP TURN command is not implemented');
|
| 1148 |
+
$this->edebug('SMTP NOTICE: ' . $this->error['error'], self::DEBUG_CLIENT);
|
| 1149 |
+
|
| 1150 |
+
return false;
|
| 1151 |
+
}
|
| 1152 |
+
|
| 1153 |
+
/**
|
| 1154 |
+
* Send raw data to the server.
|
| 1155 |
+
*
|
| 1156 |
+
* @param string $data The data to send
|
| 1157 |
+
* @param string $command Optionally, the command this is part of, used only for controlling debug output
|
| 1158 |
+
*
|
| 1159 |
+
* @return int|bool The number of bytes sent to the server or false on error
|
| 1160 |
+
*/
|
| 1161 |
+
public function client_send($data, $command = '')
|
| 1162 |
+
{
|
| 1163 |
+
//If SMTP transcripts are left enabled, or debug output is posted online
|
| 1164 |
+
//it can leak credentials, so hide credentials in all but lowest level
|
| 1165 |
+
if (
|
| 1166 |
+
self::DEBUG_LOWLEVEL > $this->do_debug &&
|
| 1167 |
+
in_array($command, ['User & Password', 'Username', 'Password'], true)
|
| 1168 |
+
) {
|
| 1169 |
+
$this->edebug('CLIENT -> SERVER: [credentials hidden]', self::DEBUG_CLIENT);
|
| 1170 |
+
} else {
|
| 1171 |
+
$this->edebug('CLIENT -> SERVER: ' . $data, self::DEBUG_CLIENT);
|
| 1172 |
+
}
|
| 1173 |
+
set_error_handler(function () {
|
| 1174 |
+
call_user_func_array([$this, 'errorHandler'], func_get_args());
|
| 1175 |
+
});
|
| 1176 |
+
$result = fwrite($this->smtp_conn, $data);
|
| 1177 |
+
restore_error_handler();
|
| 1178 |
+
|
| 1179 |
+
return $result;
|
| 1180 |
+
}
|
| 1181 |
+
|
| 1182 |
+
/**
|
| 1183 |
+
* Get the latest error.
|
| 1184 |
+
*
|
| 1185 |
+
* @return array
|
| 1186 |
+
*/
|
| 1187 |
+
public function getError()
|
| 1188 |
+
{
|
| 1189 |
+
return $this->error;
|
| 1190 |
+
}
|
| 1191 |
+
|
| 1192 |
+
/**
|
| 1193 |
+
* Get SMTP extensions available on the server.
|
| 1194 |
+
*
|
| 1195 |
+
* @return array|null
|
| 1196 |
+
*/
|
| 1197 |
+
public function getServerExtList()
|
| 1198 |
+
{
|
| 1199 |
+
return $this->server_caps;
|
| 1200 |
+
}
|
| 1201 |
+
|
| 1202 |
+
/**
|
| 1203 |
+
* Get metadata about the SMTP server from its HELO/EHLO response.
|
| 1204 |
+
* The method works in three ways, dependent on argument value and current state:
|
| 1205 |
+
* 1. HELO/EHLO has not been sent - returns null and populates $this->error.
|
| 1206 |
+
* 2. HELO has been sent -
|
| 1207 |
+
* $name == 'HELO': returns server name
|
| 1208 |
+
* $name == 'EHLO': returns boolean false
|
| 1209 |
+
* $name == any other string: returns null and populates $this->error
|
| 1210 |
+
* 3. EHLO has been sent -
|
| 1211 |
+
* $name == 'HELO'|'EHLO': returns the server name
|
| 1212 |
+
* $name == any other string: if extension $name exists, returns True
|
| 1213 |
+
* or its options (e.g. AUTH mechanisms supported). Otherwise returns False.
|
| 1214 |
+
*
|
| 1215 |
+
* @param string $name Name of SMTP extension or 'HELO'|'EHLO'
|
| 1216 |
+
*
|
| 1217 |
+
* @return string|bool|null
|
| 1218 |
+
*/
|
| 1219 |
+
public function getServerExt($name)
|
| 1220 |
+
{
|
| 1221 |
+
if (!$this->server_caps) {
|
| 1222 |
+
$this->setError('No HELO/EHLO was sent');
|
| 1223 |
+
|
| 1224 |
+
return null;
|
| 1225 |
+
}
|
| 1226 |
+
|
| 1227 |
+
if (!array_key_exists($name, $this->server_caps)) {
|
| 1228 |
+
if ('HELO' === $name) {
|
| 1229 |
+
return $this->server_caps['EHLO'];
|
| 1230 |
+
}
|
| 1231 |
+
if ('EHLO' === $name || array_key_exists('EHLO', $this->server_caps)) {
|
| 1232 |
+
return false;
|
| 1233 |
+
}
|
| 1234 |
+
$this->setError('HELO handshake was used; No information about server extensions available');
|
| 1235 |
+
|
| 1236 |
+
return null;
|
| 1237 |
+
}
|
| 1238 |
+
|
| 1239 |
+
return $this->server_caps[$name];
|
| 1240 |
+
}
|
| 1241 |
+
|
| 1242 |
+
/**
|
| 1243 |
+
* Get the last reply from the server.
|
| 1244 |
+
*
|
| 1245 |
+
* @return string
|
| 1246 |
+
*/
|
| 1247 |
+
public function getLastReply()
|
| 1248 |
+
{
|
| 1249 |
+
return $this->last_reply;
|
| 1250 |
+
}
|
| 1251 |
+
|
| 1252 |
+
/**
|
| 1253 |
+
* Read the SMTP server's response.
|
| 1254 |
+
* Either before eof or socket timeout occurs on the operation.
|
| 1255 |
+
* With SMTP we can tell if we have more lines to read if the
|
| 1256 |
+
* 4th character is '-' symbol. If it is a space then we don't
|
| 1257 |
+
* need to read anything else.
|
| 1258 |
+
*
|
| 1259 |
+
* @return string
|
| 1260 |
+
*/
|
| 1261 |
+
protected function get_lines()
|
| 1262 |
+
{
|
| 1263 |
+
//If the connection is bad, give up straight away
|
| 1264 |
+
if (!is_resource($this->smtp_conn)) {
|
| 1265 |
+
return '';
|
| 1266 |
+
}
|
| 1267 |
+
$data = '';
|
| 1268 |
+
$endtime = 0;
|
| 1269 |
+
stream_set_timeout($this->smtp_conn, $this->Timeout);
|
| 1270 |
+
if ($this->Timelimit > 0) {
|
| 1271 |
+
$endtime = time() + $this->Timelimit;
|
| 1272 |
+
}
|
| 1273 |
+
$selR = [$this->smtp_conn];
|
| 1274 |
+
$selW = null;
|
| 1275 |
+
while (is_resource($this->smtp_conn) && !feof($this->smtp_conn)) {
|
| 1276 |
+
//Must pass vars in here as params are by reference
|
| 1277 |
+
//solution for signals inspired by https://github.com/symfony/symfony/pull/6540
|
| 1278 |
+
set_error_handler(function () {
|
| 1279 |
+
call_user_func_array([$this, 'errorHandler'], func_get_args());
|
| 1280 |
+
});
|
| 1281 |
+
$n = stream_select($selR, $selW, $selW, $this->Timelimit);
|
| 1282 |
+
restore_error_handler();
|
| 1283 |
+
|
| 1284 |
+
if ($n === false) {
|
| 1285 |
+
$message = $this->getError()['detail'];
|
| 1286 |
+
|
| 1287 |
+
$this->edebug(
|
| 1288 |
+
'SMTP -> get_lines(): select failed (' . $message . ')',
|
| 1289 |
+
self::DEBUG_LOWLEVEL
|
| 1290 |
+
);
|
| 1291 |
+
|
| 1292 |
+
//stream_select returns false when the `select` system call is interrupted
|
| 1293 |
+
//by an incoming signal, try the select again
|
| 1294 |
+
if (stripos($message, 'interrupted system call') !== false) {
|
| 1295 |
+
$this->edebug(
|
| 1296 |
+
'SMTP -> get_lines(): retrying stream_select',
|
| 1297 |
+
self::DEBUG_LOWLEVEL
|
| 1298 |
+
);
|
| 1299 |
+
$this->setError('');
|
| 1300 |
+
continue;
|
| 1301 |
+
}
|
| 1302 |
+
|
| 1303 |
+
break;
|
| 1304 |
+
}
|
| 1305 |
+
|
| 1306 |
+
if (!$n) {
|
| 1307 |
+
$this->edebug(
|
| 1308 |
+
'SMTP -> get_lines(): select timed-out in (' . $this->Timelimit . ' sec)',
|
| 1309 |
+
self::DEBUG_LOWLEVEL
|
| 1310 |
+
);
|
| 1311 |
+
break;
|
| 1312 |
+
}
|
| 1313 |
+
|
| 1314 |
+
//Deliberate noise suppression - errors are handled afterwards
|
| 1315 |
+
$str = @fgets($this->smtp_conn, self::MAX_REPLY_LENGTH);
|
| 1316 |
+
$this->edebug('SMTP INBOUND: "' . trim($str) . '"', self::DEBUG_LOWLEVEL);
|
| 1317 |
+
$data .= $str;
|
| 1318 |
+
//If response is only 3 chars (not valid, but RFC5321 S4.2 says it must be handled),
|
| 1319 |
+
//or 4th character is a space or a line break char, we are done reading, break the loop.
|
| 1320 |
+
//String array access is a significant micro-optimisation over strlen
|
| 1321 |
+
if (!isset($str[3]) || $str[3] === ' ' || $str[3] === "\r" || $str[3] === "\n") {
|
| 1322 |
+
break;
|
| 1323 |
+
}
|
| 1324 |
+
//Timed-out? Log and break
|
| 1325 |
+
$info = stream_get_meta_data($this->smtp_conn);
|
| 1326 |
+
if ($info['timed_out']) {
|
| 1327 |
+
$this->edebug(
|
| 1328 |
+
'SMTP -> get_lines(): stream timed-out (' . $this->Timeout . ' sec)',
|
| 1329 |
+
self::DEBUG_LOWLEVEL
|
| 1330 |
+
);
|
| 1331 |
+
break;
|
| 1332 |
+
}
|
| 1333 |
+
//Now check if reads took too long
|
| 1334 |
+
if ($endtime && time() > $endtime) {
|
| 1335 |
+
$this->edebug(
|
| 1336 |
+
'SMTP -> get_lines(): timelimit reached (' .
|
| 1337 |
+
$this->Timelimit . ' sec)',
|
| 1338 |
+
self::DEBUG_LOWLEVEL
|
| 1339 |
+
);
|
| 1340 |
+
break;
|
| 1341 |
+
}
|
| 1342 |
+
}
|
| 1343 |
+
|
| 1344 |
+
return $data;
|
| 1345 |
+
}
|
| 1346 |
+
|
| 1347 |
+
/**
|
| 1348 |
+
* Enable or disable VERP address generation.
|
| 1349 |
+
*
|
| 1350 |
+
* @param bool $enabled
|
| 1351 |
+
*/
|
| 1352 |
+
public function setVerp($enabled = false)
|
| 1353 |
+
{
|
| 1354 |
+
$this->do_verp = $enabled;
|
| 1355 |
+
}
|
| 1356 |
+
|
| 1357 |
+
/**
|
| 1358 |
+
* Get VERP address generation mode.
|
| 1359 |
+
*
|
| 1360 |
+
* @return bool
|
| 1361 |
+
*/
|
| 1362 |
+
public function getVerp()
|
| 1363 |
+
{
|
| 1364 |
+
return $this->do_verp;
|
| 1365 |
+
}
|
| 1366 |
+
|
| 1367 |
+
/**
|
| 1368 |
+
* Set error messages and codes.
|
| 1369 |
+
*
|
| 1370 |
+
* @param string $message The error message
|
| 1371 |
+
* @param string $detail Further detail on the error
|
| 1372 |
+
* @param string $smtp_code An associated SMTP error code
|
| 1373 |
+
* @param string $smtp_code_ex Extended SMTP code
|
| 1374 |
+
*/
|
| 1375 |
+
protected function setError($message, $detail = '', $smtp_code = '', $smtp_code_ex = '')
|
| 1376 |
+
{
|
| 1377 |
+
$this->error = [
|
| 1378 |
+
'error' => $message,
|
| 1379 |
+
'detail' => $detail,
|
| 1380 |
+
'smtp_code' => $smtp_code,
|
| 1381 |
+
'smtp_code_ex' => $smtp_code_ex,
|
| 1382 |
+
];
|
| 1383 |
+
}
|
| 1384 |
+
|
| 1385 |
+
/**
|
| 1386 |
+
* Set debug output method.
|
| 1387 |
+
*
|
| 1388 |
+
* @param string|callable $method The name of the mechanism to use for debugging output, or a callable to handle it
|
| 1389 |
+
*/
|
| 1390 |
+
public function setDebugOutput($method = 'echo')
|
| 1391 |
+
{
|
| 1392 |
+
$this->Debugoutput = $method;
|
| 1393 |
+
}
|
| 1394 |
+
|
| 1395 |
+
/**
|
| 1396 |
+
* Get debug output method.
|
| 1397 |
+
*
|
| 1398 |
+
* @return string
|
| 1399 |
+
*/
|
| 1400 |
+
public function getDebugOutput()
|
| 1401 |
+
{
|
| 1402 |
+
return $this->Debugoutput;
|
| 1403 |
+
}
|
| 1404 |
+
|
| 1405 |
+
/**
|
| 1406 |
+
* Set debug output level.
|
| 1407 |
+
*
|
| 1408 |
+
* @param int $level
|
| 1409 |
+
*/
|
| 1410 |
+
public function setDebugLevel($level = 0)
|
| 1411 |
+
{
|
| 1412 |
+
$this->do_debug = $level;
|
| 1413 |
+
}
|
| 1414 |
+
|
| 1415 |
+
/**
|
| 1416 |
+
* Get debug output level.
|
| 1417 |
+
*
|
| 1418 |
+
* @return int
|
| 1419 |
+
*/
|
| 1420 |
+
public function getDebugLevel()
|
| 1421 |
+
{
|
| 1422 |
+
return $this->do_debug;
|
| 1423 |
+
}
|
| 1424 |
+
|
| 1425 |
+
/**
|
| 1426 |
+
* Set SMTP timeout.
|
| 1427 |
+
*
|
| 1428 |
+
* @param int $timeout The timeout duration in seconds
|
| 1429 |
+
*/
|
| 1430 |
+
public function setTimeout($timeout = 0)
|
| 1431 |
+
{
|
| 1432 |
+
$this->Timeout = $timeout;
|
| 1433 |
+
}
|
| 1434 |
+
|
| 1435 |
+
/**
|
| 1436 |
+
* Get SMTP timeout.
|
| 1437 |
+
*
|
| 1438 |
+
* @return int
|
| 1439 |
+
*/
|
| 1440 |
+
public function getTimeout()
|
| 1441 |
+
{
|
| 1442 |
+
return $this->Timeout;
|
| 1443 |
+
}
|
| 1444 |
+
|
| 1445 |
+
/**
|
| 1446 |
+
* Reports an error number and string.
|
| 1447 |
+
*
|
| 1448 |
+
* @param int $errno The error number returned by PHP
|
| 1449 |
+
* @param string $errmsg The error message returned by PHP
|
| 1450 |
+
* @param string $errfile The file the error occurred in
|
| 1451 |
+
* @param int $errline The line number the error occurred on
|
| 1452 |
+
*/
|
| 1453 |
+
protected function errorHandler($errno, $errmsg, $errfile = '', $errline = 0)
|
| 1454 |
+
{
|
| 1455 |
+
$notice = 'Connection failed.';
|
| 1456 |
+
$this->setError(
|
| 1457 |
+
$notice,
|
| 1458 |
+
$errmsg,
|
| 1459 |
+
(string) $errno
|
| 1460 |
+
);
|
| 1461 |
+
$this->edebug(
|
| 1462 |
+
"$notice Error #$errno: $errmsg [$errfile line $errline]",
|
| 1463 |
+
self::DEBUG_CONNECTION
|
| 1464 |
+
);
|
| 1465 |
+
}
|
| 1466 |
+
|
| 1467 |
+
/**
|
| 1468 |
+
* Extract and return the ID of the last SMTP transaction based on
|
| 1469 |
+
* a list of patterns provided in SMTP::$smtp_transaction_id_patterns.
|
| 1470 |
+
* Relies on the host providing the ID in response to a DATA command.
|
| 1471 |
+
* If no reply has been received yet, it will return null.
|
| 1472 |
+
* If no pattern was matched, it will return false.
|
| 1473 |
+
*
|
| 1474 |
+
* @return bool|string|null
|
| 1475 |
+
*/
|
| 1476 |
+
protected function recordLastTransactionID()
|
| 1477 |
+
{
|
| 1478 |
+
$reply = $this->getLastReply();
|
| 1479 |
+
|
| 1480 |
+
if (empty($reply)) {
|
| 1481 |
+
$this->last_smtp_transaction_id = null;
|
| 1482 |
+
} else {
|
| 1483 |
+
$this->last_smtp_transaction_id = false;
|
| 1484 |
+
foreach ($this->smtp_transaction_id_patterns as $smtp_transaction_id_pattern) {
|
| 1485 |
+
$matches = [];
|
| 1486 |
+
if (preg_match($smtp_transaction_id_pattern, $reply, $matches)) {
|
| 1487 |
+
$this->last_smtp_transaction_id = trim($matches[1]);
|
| 1488 |
+
break;
|
| 1489 |
+
}
|
| 1490 |
+
}
|
| 1491 |
+
}
|
| 1492 |
+
|
| 1493 |
+
return $this->last_smtp_transaction_id;
|
| 1494 |
+
}
|
| 1495 |
+
|
| 1496 |
+
/**
|
| 1497 |
+
* Get the queue/transaction ID of the last SMTP transaction
|
| 1498 |
+
* If no reply has been received yet, it will return null.
|
| 1499 |
+
* If no pattern was matched, it will return false.
|
| 1500 |
+
*
|
| 1501 |
+
* @return bool|string|null
|
| 1502 |
+
*
|
| 1503 |
+
* @see recordLastTransactionID()
|
| 1504 |
+
*/
|
| 1505 |
+
public function getLastTransactionID()
|
| 1506 |
+
{
|
| 1507 |
+
return $this->last_smtp_transaction_id;
|
| 1508 |
+
}
|
| 1509 |
+
}
|
backup_2026-05-04/PROJECT_OVERVIEW.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Martechsol Chat Assistant - Project Overview
|
| 2 |
+
|
| 3 |
+
This document explains how your RAG (Retrieval-Augmented Generation) chatbot works in simple terms.
|
| 4 |
+
|
| 5 |
+
## 1. The "Brain" (The AI Model)
|
| 6 |
+
The assistant uses a hybrid LLM approach:
|
| 7 |
+
- **Qwen 2.5 32B** (specifically `qwen/qwen3-32b`) as the main reasoning and generation engine.
|
| 8 |
+
- **Llama 3.1 8B** (specifically `llama-3.1-8b-instant`) as a lightning-fast model for query rewriting and expansion.
|
| 9 |
+
- **Provider**: Both are powered by **Groq**, enabling near-instantaneous responses.
|
| 10 |
+
|
| 11 |
+
## 2. How it Works (The "RAG" Process)
|
| 12 |
+
Instead of just relying on general knowledge, this bot "reads" your documents to give specific answers.
|
| 13 |
+
1. **Reading**: It looks at your files in the `docs/` folder (PDFs and Text files).
|
| 14 |
+
2. **Memorizing**: It breaks the text into small chunks and converts them into mathematical "vectors" (using the `bge-small-en-v1.5` model).
|
| 15 |
+
3. **Searching**: When you ask a question, it expands the query using Llama 3.1 8B, then performs a **Hybrid Search** combining Dense vectors (**FAISS**) and Keyword search (**BM25**).
|
| 16 |
+
4. **Reranking**: It deeply evaluates the top retrieved chunks using `bge-reranker-base` to ensure maximum relevance.
|
| 17 |
+
5. **Answering**: It sends your question along with the most relevant document parts to the Qwen 2.5 AI, which then writes a highly precise, formatted reply.
|
| 18 |
+
|
| 19 |
+
## 3. The Architecture
|
| 20 |
+
### Frontend (The Face)
|
| 21 |
+
- **Gradio**: This is the clean, chat-like interface you see. It is hosted on **Hugging Face Spaces**.
|
| 22 |
+
- **WordPress Addon**: A custom HTML/CSS/JS wrapper that lets you embed the chat as a beautiful floating button on your website.
|
| 23 |
+
|
| 24 |
+
### Backend (The Engine)
|
| 25 |
+
- **FastAPI**: A high-performance Python framework that connects everything. It manages the messages, handles the document search, and talks to the AI provider.
|
| 26 |
+
- **Uvicorn**: The lightning-fast server that runs the FastAPI code.
|
| 27 |
+
|
| 28 |
+
## 4. Key Features
|
| 29 |
+
- **Humanistic Replies**: The bot is programmed to be polite, conversational, and professional.
|
| 30 |
+
- **Context-Aware**: It remembers the last few messages in the conversation so you can ask follow-up questions.
|
| 31 |
+
- **Greeting Support**: It can handle "Hi" and "Hello" naturally before getting down to business.
|
| 32 |
+
- **Safe & Grounded**: It is instructed to only answer based on your documents to prevent "hallucinations" (making things up).
|
backup_2026-05-04/README.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Fast RAG Chatbot
|
| 3 |
+
emoji: 🤖
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
storage: true
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# High-Performance RAG Chatbot (FastAPI + FAISS)
|
| 12 |
+
|
| 13 |
+
Production-style document QA chatbot using:
|
| 14 |
+
- FastAPI API service
|
| 15 |
+
- FAISS vector search
|
| 16 |
+
- SentenceTransformer embeddings (`BAAI/bge-small-en-v1.5` by default)
|
| 17 |
+
- Groq (preferred) or Hugging Face LLM APIs
|
| 18 |
+
- Optional Gradio chat UI
|
| 19 |
+
|
| 20 |
+
## Features
|
| 21 |
+
|
| 22 |
+
- Loads `.pdf` and `.txt` files from `docs/`
|
| 23 |
+
- Cleans extracted text and chunks into semantic windows
|
| 24 |
+
- Chunk size: 420 tokens (word-level approximation)
|
| 25 |
+
- Overlap: 80 tokens
|
| 26 |
+
- Builds FAISS index and saves it locally
|
| 27 |
+
- Re-indexes only when docs change (fingerprint-based cache)
|
| 28 |
+
- Retrieves top-k relevant chunks only (default k=4)
|
| 29 |
+
- Strict anti-hallucination prompt
|
| 30 |
+
- Health endpoint with docs/index status
|
| 31 |
+
- Retrieval logging (source + similarity score)
|
| 32 |
+
- CORS controls for website integration
|
| 33 |
+
- Optional API key auth for `/chat`
|
| 34 |
+
- In-memory rate limiting per client IP
|
| 35 |
+
- Query embedding cache for repeated questions
|
| 36 |
+
- Docker + docker-compose deployment
|
| 37 |
+
|
| 38 |
+
## Project Structure
|
| 39 |
+
|
| 40 |
+
`app/main.py` - FastAPI app and endpoints
|
| 41 |
+
`app/services/document_loader.py` - PDF/TXT ingestion and cleaning
|
| 42 |
+
`app/services/chunker.py` - token-window chunking
|
| 43 |
+
`app/services/embeddings.py` - embedding model wrapper
|
| 44 |
+
`app/services/vector_store.py` - FAISS index and retrieval
|
| 45 |
+
`app/services/llm.py` - Groq/HF LLM clients and prompt
|
| 46 |
+
`app/services/rag_pipeline.py` - end-to-end chat flow
|
| 47 |
+
`app/ui_gradio.py` - optional web chat UI
|
| 48 |
+
|
| 49 |
+
## Setup
|
| 50 |
+
|
| 51 |
+
1. Install dependencies:
|
| 52 |
+
|
| 53 |
+
```bash
|
| 54 |
+
pip install -r requirements.txt
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
2. Configure environment:
|
| 58 |
+
|
| 59 |
+
```bash
|
| 60 |
+
copy .env.example .env
|
| 61 |
+
```
|
| 62 |
+
|
| 63 |
+
Then set your keys in `.env`:
|
| 64 |
+
- `GROQ_API_KEY` (if using Groq)
|
| 65 |
+
- `HF_API_KEY` (if using Hugging Face)
|
| 66 |
+
- Optional:
|
| 67 |
+
- `API_KEY` for request auth (send as `x-api-key`)
|
| 68 |
+
- `CORS_ALLOW_ORIGINS` as comma-separated origins
|
| 69 |
+
- `RATE_LIMIT_REQUESTS` and `RATE_LIMIT_WINDOW_SECONDS`
|
| 70 |
+
|
| 71 |
+
3. Add documents:
|
| 72 |
+
- Put your `.pdf` and `.txt` files in `docs/`
|
| 73 |
+
|
| 74 |
+
## Run API
|
| 75 |
+
|
| 76 |
+
```bash
|
| 77 |
+
uvicorn app.main:app --host 0.0.0.0 --port 8000
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
## Endpoints
|
| 81 |
+
|
| 82 |
+
### `GET /health`
|
| 83 |
+
Returns status and index readiness.
|
| 84 |
+
|
| 85 |
+
### `POST /chat`
|
| 86 |
+
|
| 87 |
+
Request:
|
| 88 |
+
```json
|
| 89 |
+
{
|
| 90 |
+
"message": "What are the key points?",
|
| 91 |
+
"history": []
|
| 92 |
+
}
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
Response:
|
| 96 |
+
```json
|
| 97 |
+
{
|
| 98 |
+
"reply": "Answer based on retrieved context.",
|
| 99 |
+
"retrieved_chunks": [
|
| 100 |
+
{
|
| 101 |
+
"id": "...",
|
| 102 |
+
"source": "...",
|
| 103 |
+
"text": "...",
|
| 104 |
+
"score": 0.83
|
| 105 |
+
}
|
| 106 |
+
]
|
| 107 |
+
}
|
| 108 |
+
```
|
| 109 |
+
|
| 110 |
+
## Optional UI
|
| 111 |
+
|
| 112 |
+
Start API first, then:
|
| 113 |
+
|
| 114 |
+
```bash
|
| 115 |
+
python -m app.ui_gradio
|
| 116 |
+
```
|
| 117 |
+
|
| 118 |
+
By default, Gradio now runs in direct RAG mode (no localhost API dependency).
|
| 119 |
+
If you set `RAG_API_URL`, it will call that external FastAPI endpoint instead.
|
| 120 |
+
|
| 121 |
+
## Deployment Notes
|
| 122 |
+
|
| 123 |
+
- Works as backend for websites (REST API is frontend-agnostic)
|
| 124 |
+
- Persist `data/index/` volume in production
|
| 125 |
+
- Prefer Groq provider for low latency
|
| 126 |
+
- Keep `top_k` small (3-5) for speed and lower prompt tokens
|
| 127 |
+
- Protect `/chat` with `API_KEY` in production
|
| 128 |
+
- Set strict `CORS_ALLOW_ORIGINS` instead of `*`
|
| 129 |
+
|
| 130 |
+
## Docker Deployment
|
| 131 |
+
|
| 132 |
+
Build and run:
|
| 133 |
+
|
| 134 |
+
```bash
|
| 135 |
+
docker compose up --build -d
|
| 136 |
+
```
|
| 137 |
+
|
| 138 |
+
Health check:
|
| 139 |
+
|
| 140 |
+
```bash
|
| 141 |
+
curl http://localhost:8000/health
|
| 142 |
+
```
|
| 143 |
+
|
| 144 |
+
Chat call with API key:
|
| 145 |
+
|
| 146 |
+
```bash
|
| 147 |
+
curl -X POST http://localhost:8000/chat ^
|
| 148 |
+
-H "Content-Type: application/json" ^
|
| 149 |
+
-H "x-api-key: YOUR_API_KEY" ^
|
| 150 |
+
-d "{\"message\":\"What does the handbook say about leave policy?\",\"history\":[]}"
|
| 151 |
+
```
|
| 152 |
+
|
| 153 |
+
## Hugging Face Spaces (Recommended: Gradio Space)
|
| 154 |
+
|
| 155 |
+
Use these settings in your Space:
|
| 156 |
+
- **SDK**: Gradio
|
| 157 |
+
- **App file**: `app.py`
|
| 158 |
+
- **Python version**: 3.10+ (3.11 recommended)
|
| 159 |
+
|
| 160 |
+
Add Space Secrets:
|
| 161 |
+
- `GROQ_API_KEY` (or `HF_API_KEY`)
|
| 162 |
+
- Optional: `LLM_PROVIDER`, `GROQ_MODEL`, `HF_MODEL`, `TOP_K`
|
| 163 |
+
|
| 164 |
+
Upload project files (excluding `.env`) and include your knowledge files inside `docs/`.
|
backup_2026-05-04/app.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import uvicorn
|
| 2 |
+
from app.main import app
|
| 3 |
+
|
| 4 |
+
# Hugging Face Spaces (sdk: gradio) looks for `app` variable
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
uvicorn.run("app.main:app", host="0.0.0.0", port=7860)
|
backup_2026-05-04/app/__init__.py
ADDED
|
File without changes
|
backup_2026-05-04/app/admin/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# app/admin/__init__.py
|
backup_2026-05-04/app/admin/router.py
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
app/admin/router.py
|
| 3 |
+
───────────────────
|
| 4 |
+
Secure FastAPI router for the Admin Panel.
|
| 5 |
+
|
| 6 |
+
Security design:
|
| 7 |
+
• All /admin/* routes require HTTP Basic Auth.
|
| 8 |
+
• Credentials are compared using secrets.compare_digest() to prevent
|
| 9 |
+
timing-based attacks — NOT a simple == comparison.
|
| 10 |
+
• Wrong credentials always return 401 with WWW-Authenticate header.
|
| 11 |
+
• Admin routes are completely isolated from the main app and Gradio UI.
|
| 12 |
+
|
| 13 |
+
Endpoints:
|
| 14 |
+
GET /admin → Serves the premium admin.html SPA
|
| 15 |
+
GET /api/admin/sessions → Returns all session summaries (JSON)
|
| 16 |
+
GET /api/admin/sessions/{session_id} → Returns full session transcript
|
| 17 |
+
DELETE /api/admin/sessions/{session_id} → Deletes a session
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
import secrets
|
| 21 |
+
import logging
|
| 22 |
+
import asyncio
|
| 23 |
+
import time
|
| 24 |
+
import random
|
| 25 |
+
import smtplib
|
| 26 |
+
from email.mime.text import MIMEText
|
| 27 |
+
from email.mime.multipart import MIMEMultipart
|
| 28 |
+
from pathlib import Path
|
| 29 |
+
from typing import Dict
|
| 30 |
+
|
| 31 |
+
from fastapi import APIRouter, Depends, HTTPException, status
|
| 32 |
+
from fastapi.responses import HTMLResponse, JSONResponse
|
| 33 |
+
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
| 34 |
+
|
| 35 |
+
from app.services.session_store import get_all_sessions, get_session, delete_session, _list_json_files, _read_json
|
| 36 |
+
from app.core.config import get_settings
|
| 37 |
+
import pandas as pd
|
| 38 |
+
import io
|
| 39 |
+
from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse
|
| 40 |
+
|
| 41 |
+
logger = logging.getLogger(__name__)
|
| 42 |
+
settings = get_settings()
|
| 43 |
+
|
| 44 |
+
# ── Credentials (stored as constants — in production, move to env vars) ───────
|
| 45 |
+
ADMIN_USERNAME = "martech_admin"
|
| 46 |
+
ADMIN_PASSWORD = "martech_admin_303"
|
| 47 |
+
|
| 48 |
+
# ── Security scheme ───────────────────────────────────────────────────────────
|
| 49 |
+
security = HTTPBasic()
|
| 50 |
+
|
| 51 |
+
# ── Router ────────────────────────────────────────────────────────────────────
|
| 52 |
+
admin_router = APIRouter(tags=["admin"])
|
| 53 |
+
|
| 54 |
+
# ── OTP Store (In-memory for simplicity) ──────────────────────────────────────
|
| 55 |
+
# Format: {username: {"code": "123456", "expires": timestamp}}
|
| 56 |
+
_otp_store: Dict[str, dict] = {}
|
| 57 |
+
OTP_EXPIRY_SECONDS = 300 # 5 minutes
|
| 58 |
+
|
| 59 |
+
# ── Path to the admin HTML template ──────────────────────────────────────────
|
| 60 |
+
_ADMIN_HTML_PATH = Path(__file__).parent / "templates" / "admin.html"
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
# ── Auth dependency ───────────────────────────────────────────────────────────
|
| 64 |
+
def verify_admin(credentials: HTTPBasicCredentials = Depends(security)) -> str:
|
| 65 |
+
"""
|
| 66 |
+
Verifies HTTP Basic credentials using constant-time comparison.
|
| 67 |
+
Raises 401 for any mismatch. Returns the username on success.
|
| 68 |
+
"""
|
| 69 |
+
correct_username = secrets.compare_digest(
|
| 70 |
+
credentials.username.encode("utf-8"),
|
| 71 |
+
ADMIN_USERNAME.encode("utf-8"),
|
| 72 |
+
)
|
| 73 |
+
correct_password = secrets.compare_digest(
|
| 74 |
+
credentials.password.encode("utf-8"),
|
| 75 |
+
ADMIN_PASSWORD.encode("utf-8"),
|
| 76 |
+
)
|
| 77 |
+
if not (correct_username and correct_password):
|
| 78 |
+
logger.warning(
|
| 79 |
+
"Admin auth failed for user: '%s'", credentials.username
|
| 80 |
+
)
|
| 81 |
+
raise HTTPException(
|
| 82 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 83 |
+
detail="Invalid admin credentials.",
|
| 84 |
+
)
|
| 85 |
+
return credentials.username
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
# ── OTP Endpoints ─────────────────────────────────────────────────────────────
|
| 89 |
+
|
| 90 |
+
from pydantic import BaseModel
|
| 91 |
+
|
| 92 |
+
class LoginRequest(BaseModel):
|
| 93 |
+
username: str
|
| 94 |
+
password: str
|
| 95 |
+
|
| 96 |
+
class OTPVerifyRequest(BaseModel):
|
| 97 |
+
username: str
|
| 98 |
+
password: str
|
| 99 |
+
code: str
|
| 100 |
+
|
| 101 |
+
@admin_router.post("/api/admin/request-otp", include_in_schema=False)
|
| 102 |
+
async def request_otp(req: LoginRequest):
|
| 103 |
+
"""
|
| 104 |
+
Verifies password and generates a 6-digit OTP.
|
| 105 |
+
Sends the OTP to the configured admin email.
|
| 106 |
+
"""
|
| 107 |
+
if not (secrets.compare_digest(req.username, ADMIN_USERNAME) and
|
| 108 |
+
secrets.compare_digest(req.password, ADMIN_PASSWORD)):
|
| 109 |
+
raise HTTPException(status_code=401, detail="Invalid credentials.")
|
| 110 |
+
|
| 111 |
+
# Generate 6-digit code
|
| 112 |
+
code = "".join([str(random.randint(0, 9)) for _ in range(6)])
|
| 113 |
+
_otp_store[req.username] = {
|
| 114 |
+
"code": code,
|
| 115 |
+
"expires": time.time() + OTP_EXPIRY_SECONDS
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
# Attempt to send email
|
| 119 |
+
email_sent = False
|
| 120 |
+
if settings.smtp_user and settings.smtp_pass:
|
| 121 |
+
try:
|
| 122 |
+
# Use to_thread to avoid blocking the event loop during SMTP network I/O
|
| 123 |
+
await asyncio.to_thread(send_otp_email, settings.admin_email, code)
|
| 124 |
+
email_sent = True
|
| 125 |
+
logger.info("OTP email sent to %s", settings.admin_email)
|
| 126 |
+
except Exception as e:
|
| 127 |
+
logger.error("Failed to send OTP email: %s", e)
|
| 128 |
+
else:
|
| 129 |
+
logger.warning("SMTP credentials not configured. OTP will only be in logs.")
|
| 130 |
+
|
| 131 |
+
# FALLBACK/LOG: Always log to console so the user is never locked out
|
| 132 |
+
print("\n" + "="*50)
|
| 133 |
+
print(f" ADMIN OTP FOR {req.username.upper()}: {code}")
|
| 134 |
+
print(f" SENDING TO: {settings.admin_email} ({'SUCCESS' if email_sent else 'FAILED/SKIPPED'})")
|
| 135 |
+
print(" EXPIRES IN 5 MINUTES")
|
| 136 |
+
print("="*50 + "\n")
|
| 137 |
+
|
| 138 |
+
return {
|
| 139 |
+
"message": "OTP sent successfully.",
|
| 140 |
+
"email_status": "sent" if email_sent else "logged_to_console_only"
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
import socket
|
| 145 |
+
|
| 146 |
+
def send_otp_email(to_email: str, otp_code: str):
|
| 147 |
+
"""Sends a professional OTP email via SMTP. Run in thread."""
|
| 148 |
+
msg = MIMEMultipart()
|
| 149 |
+
msg['From'] = settings.smtp_user
|
| 150 |
+
msg['To'] = to_email
|
| 151 |
+
msg['Subject'] = f"Your Admin Access Code: {otp_code}"
|
| 152 |
+
|
| 153 |
+
body = f"""
|
| 154 |
+
<html>
|
| 155 |
+
<body style="font-family: sans-serif; padding: 20px; color: #333;">
|
| 156 |
+
<h2 style="color: #3b82f6;">Martechsol Admin Panel</h2>
|
| 157 |
+
<p>Your one-time security code is:</p>
|
| 158 |
+
<div style="font-size: 32px; font-weight: bold; letter-spacing: 5px; padding: 15px; background: #f3f4f6; border-radius: 8px; display: inline-block; margin: 10px 0;">
|
| 159 |
+
{otp_code}
|
| 160 |
+
</div>
|
| 161 |
+
<p style="color: #666; font-size: 14px;">This code will expire in 5 minutes.</p>
|
| 162 |
+
<hr style="border: 0; border-top: 1px solid #eee; margin-top: 20px;">
|
| 163 |
+
<p style="font-size: 12px; color: #999;">If you did not request this code, please secure your account.</p>
|
| 164 |
+
</body>
|
| 165 |
+
</html>
|
| 166 |
+
"""
|
| 167 |
+
msg.attach(MIMEText(body, 'html'))
|
| 168 |
+
|
| 169 |
+
# Force IPv4 resolution to avoid "Network unreachable" IPv6 issues
|
| 170 |
+
try:
|
| 171 |
+
ais = socket.getaddrinfo(settings.smtp_server, settings.smtp_port, socket.AF_INET)
|
| 172 |
+
target_ip = ais[0][4][0]
|
| 173 |
+
logger.info("Resolved %s to IPv4: %s", settings.smtp_server, target_ip)
|
| 174 |
+
except Exception as e:
|
| 175 |
+
logger.error("DNS Resolution failed: %s", e)
|
| 176 |
+
target_ip = settings.smtp_server
|
| 177 |
+
|
| 178 |
+
logger.info("Attempting SMTP_SSL connection to %s:%d (via %s)",
|
| 179 |
+
settings.smtp_server, settings.smtp_port, target_ip)
|
| 180 |
+
|
| 181 |
+
# Using the IP directly can sometimes bypass DNS/routing quirks
|
| 182 |
+
with smtplib.SMTP_SSL(target_ip, settings.smtp_port, timeout=15) as server:
|
| 183 |
+
server.login(settings.smtp_user, settings.smtp_pass)
|
| 184 |
+
server.send_message(msg)
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
@admin_router.post("/api/admin/verify-otp", include_in_schema=False)
|
| 188 |
+
async def verify_otp(req: OTPVerifyRequest):
|
| 189 |
+
"""Verifies both the password and the OTP code."""
|
| 190 |
+
# 1. Verify password first
|
| 191 |
+
if not (secrets.compare_digest(req.username, ADMIN_USERNAME) and
|
| 192 |
+
secrets.compare_digest(req.password, ADMIN_PASSWORD)):
|
| 193 |
+
raise HTTPException(status_code=401, detail="Invalid credentials.")
|
| 194 |
+
|
| 195 |
+
# 2. Verify OTP
|
| 196 |
+
stored = _otp_store.get(req.username)
|
| 197 |
+
if not stored:
|
| 198 |
+
raise HTTPException(status_code=400, detail="No OTP requested for this user.")
|
| 199 |
+
|
| 200 |
+
if time.time() > stored["expires"]:
|
| 201 |
+
del _otp_store[req.username]
|
| 202 |
+
raise HTTPException(status_code=400, detail="OTP has expired. Please request a new one.")
|
| 203 |
+
|
| 204 |
+
if not secrets.compare_digest(req.code, stored["code"]):
|
| 205 |
+
raise HTTPException(status_code=401, detail="Invalid OTP code.")
|
| 206 |
+
|
| 207 |
+
# Success — clear OTP and return
|
| 208 |
+
del _otp_store[req.username]
|
| 209 |
+
return {"message": "OTP verified."}
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
# ── Routes ────────────────────────────────────────────────────────────────────
|
| 213 |
+
|
| 214 |
+
@admin_router.get("/admin", response_class=HTMLResponse, include_in_schema=False)
|
| 215 |
+
async def admin_panel():
|
| 216 |
+
"""Serves the admin panel SPA."""
|
| 217 |
+
try:
|
| 218 |
+
html_content = _ADMIN_HTML_PATH.read_text(encoding="utf-8")
|
| 219 |
+
return HTMLResponse(content=html_content, status_code=200)
|
| 220 |
+
except FileNotFoundError:
|
| 221 |
+
logger.error("admin.html template not found at %s", _ADMIN_HTML_PATH)
|
| 222 |
+
raise HTTPException(
|
| 223 |
+
status_code=500,
|
| 224 |
+
detail="Admin panel template is missing. Please redeploy.",
|
| 225 |
+
)
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
@admin_router.get("/api/admin/sessions", include_in_schema=False)
|
| 229 |
+
async def list_sessions(username: str = Depends(verify_admin)):
|
| 230 |
+
"""Returns all session summaries sorted by most recent activity."""
|
| 231 |
+
sessions = await get_all_sessions()
|
| 232 |
+
return JSONResponse(content={"sessions": sessions, "total": len(sessions)})
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
@admin_router.get("/api/admin/sessions/{session_id}", include_in_schema=False)
|
| 236 |
+
async def get_session_detail(
|
| 237 |
+
session_id: str, username: str = Depends(verify_admin)
|
| 238 |
+
):
|
| 239 |
+
"""Returns the full Q&A transcript for a specific session."""
|
| 240 |
+
# Basic input sanitisation — session_id should only be a UUID-like string
|
| 241 |
+
if not session_id.replace("-", "").replace("_", "").isalnum():
|
| 242 |
+
raise HTTPException(status_code=400, detail="Invalid session ID format.")
|
| 243 |
+
|
| 244 |
+
data = await get_session(session_id)
|
| 245 |
+
if not data:
|
| 246 |
+
raise HTTPException(
|
| 247 |
+
status_code=404,
|
| 248 |
+
detail=f"Session '{session_id}' not found.",
|
| 249 |
+
)
|
| 250 |
+
return JSONResponse(content=data)
|
| 251 |
+
|
| 252 |
+
|
| 253 |
+
@admin_router.delete("/api/admin/sessions/{session_id}", include_in_schema=False)
|
| 254 |
+
async def remove_session(
|
| 255 |
+
session_id: str, username: str = Depends(verify_admin)
|
| 256 |
+
):
|
| 257 |
+
"""Deletes a session file permanently."""
|
| 258 |
+
if not session_id.replace("-", "").replace("_", "").isalnum():
|
| 259 |
+
raise HTTPException(status_code=400, detail="Invalid session ID format.")
|
| 260 |
+
|
| 261 |
+
success = await delete_session(session_id)
|
| 262 |
+
if not success:
|
| 263 |
+
raise HTTPException(status_code=500, detail="Failed to delete session.")
|
| 264 |
+
return JSONResponse(content={"deleted": session_id})
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
# ── Export Endpoints ─────────────────────────────────────────────────────────
|
| 268 |
+
|
| 269 |
+
@admin_router.get("/api/admin/export/session/{session_id}", include_in_schema=False)
|
| 270 |
+
async def export_session(
|
| 271 |
+
session_id: str, format: str = "excel", username: str = Depends(verify_admin)
|
| 272 |
+
):
|
| 273 |
+
"""Exports a single session's transcript in Excel or JSON format."""
|
| 274 |
+
data = await get_session(session_id)
|
| 275 |
+
if not data:
|
| 276 |
+
raise HTTPException(status_code=404, detail="Session not found.")
|
| 277 |
+
|
| 278 |
+
messages = data.get("messages", [])
|
| 279 |
+
export_data = []
|
| 280 |
+
for msg in messages:
|
| 281 |
+
export_data.append({
|
| 282 |
+
"Timestamp": msg.get("timestamp", ""),
|
| 283 |
+
"Question": msg.get("question", ""),
|
| 284 |
+
"Answer": msg.get("answer", "")
|
| 285 |
+
})
|
| 286 |
+
|
| 287 |
+
if format.lower() == "json":
|
| 288 |
+
return JSONResponse(
|
| 289 |
+
content=export_data,
|
| 290 |
+
headers={"Content-Disposition": f"attachment; filename=session_{session_id}.json"}
|
| 291 |
+
)
|
| 292 |
+
|
| 293 |
+
# Excel Export
|
| 294 |
+
df = pd.DataFrame(export_data)
|
| 295 |
+
output = io.BytesIO()
|
| 296 |
+
with pd.ExcelWriter(output, engine='openpyxl') as writer:
|
| 297 |
+
df.to_excel(writer, index=False, sheet_name='Transcript')
|
| 298 |
+
|
| 299 |
+
output.seek(0)
|
| 300 |
+
return StreamingResponse(
|
| 301 |
+
output,
|
| 302 |
+
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
| 303 |
+
headers={"Content-Disposition": f"attachment; filename=session_{session_id}.xlsx"}
|
| 304 |
+
)
|
| 305 |
+
|
| 306 |
+
|
| 307 |
+
@admin_router.get("/api/admin/export/all", include_in_schema=False)
|
| 308 |
+
async def export_all_sessions(
|
| 309 |
+
format: str = "excel", username: str = Depends(verify_admin)
|
| 310 |
+
):
|
| 311 |
+
"""Exports all sessions' transcripts in a single Excel or JSON file."""
|
| 312 |
+
files = await asyncio.to_thread(_list_json_files)
|
| 313 |
+
all_messages = []
|
| 314 |
+
|
| 315 |
+
for fpath in files:
|
| 316 |
+
try:
|
| 317 |
+
data = await asyncio.to_thread(_read_json, fpath)
|
| 318 |
+
session_id = data.get("session_id", fpath.stem)
|
| 319 |
+
for msg in data.get("messages", []):
|
| 320 |
+
all_messages.append({
|
| 321 |
+
"Session ID": session_id,
|
| 322 |
+
"Timestamp": msg.get("timestamp", ""),
|
| 323 |
+
"Question": msg.get("question", ""),
|
| 324 |
+
"Answer": msg.get("answer", "")
|
| 325 |
+
})
|
| 326 |
+
except Exception as e:
|
| 327 |
+
logger.warning("Failed to read %s for export: %s", fpath, e)
|
| 328 |
+
|
| 329 |
+
if format.lower() == "json":
|
| 330 |
+
return JSONResponse(
|
| 331 |
+
content=all_messages,
|
| 332 |
+
headers={"Content-Disposition": "attachment; filename=all_chats_export.json"}
|
| 333 |
+
)
|
| 334 |
+
|
| 335 |
+
# Excel Export
|
| 336 |
+
df = pd.DataFrame(all_messages)
|
| 337 |
+
output = io.BytesIO()
|
| 338 |
+
with pd.ExcelWriter(output, engine='openpyxl') as writer:
|
| 339 |
+
df.to_excel(writer, index=False, sheet_name='All Chats')
|
| 340 |
+
|
| 341 |
+
output.seek(0)
|
| 342 |
+
return StreamingResponse(
|
| 343 |
+
output,
|
| 344 |
+
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
| 345 |
+
headers={"Content-Disposition": "attachment; filename=all_chats_export.xlsx"}
|
| 346 |
+
)
|
backup_2026-05-04/app/admin/templates/admin.html
ADDED
|
@@ -0,0 +1,1143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8" />
|
| 6 |
+
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
| 7 |
+
<title>Martechsol — Admin Panel</title>
|
| 8 |
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
| 9 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
|
| 10 |
+
<style>
|
| 11 |
+
*,
|
| 12 |
+
*::before,
|
| 13 |
+
*::after {
|
| 14 |
+
box-sizing: border-box;
|
| 15 |
+
margin: 0;
|
| 16 |
+
padding: 0
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
:root {
|
| 20 |
+
--bg: #0a0d14;
|
| 21 |
+
--surface: #111827;
|
| 22 |
+
--surface2: #1a2235;
|
| 23 |
+
--surface3: #1f2a3d;
|
| 24 |
+
--border: #1e2d45;
|
| 25 |
+
--accent: #3b82f6;
|
| 26 |
+
--accent2: #6366f1;
|
| 27 |
+
--green: #10b981;
|
| 28 |
+
--red: #ef4444;
|
| 29 |
+
--yellow: #f59e0b;
|
| 30 |
+
--text: #f1f5f9;
|
| 31 |
+
--text2: #94a3b8;
|
| 32 |
+
--text3: #64748b;
|
| 33 |
+
--radius: 12px;
|
| 34 |
+
--shadow: 0 4px 24px rgba(0, 0, 0, .4);
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
html,
|
| 38 |
+
body {
|
| 39 |
+
height: 100%;
|
| 40 |
+
font-family: 'Inter', sans-serif;
|
| 41 |
+
background: var(--bg);
|
| 42 |
+
color: var(--text);
|
| 43 |
+
overflow: hidden
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
/* ── Login Screen ── */
|
| 47 |
+
#login-screen {
|
| 48 |
+
position: fixed;
|
| 49 |
+
inset: 0;
|
| 50 |
+
display: flex;
|
| 51 |
+
align-items: center;
|
| 52 |
+
justify-content: center;
|
| 53 |
+
background: radial-gradient(ellipse at 50% 0%, #1a2a4a 0%, var(--bg) 70%);
|
| 54 |
+
z-index: 9999;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
.login-card {
|
| 58 |
+
width: 380px;
|
| 59 |
+
background: var(--surface);
|
| 60 |
+
border: 1px solid var(--border);
|
| 61 |
+
border-radius: 20px;
|
| 62 |
+
padding: 44px 40px;
|
| 63 |
+
box-shadow: var(--shadow);
|
| 64 |
+
animation: fadeUp .5s ease;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
.login-logo {
|
| 68 |
+
text-align: center;
|
| 69 |
+
margin-bottom: 28px
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
.login-logo svg {
|
| 73 |
+
width: 52px;
|
| 74 |
+
height: 52px
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
.login-logo h1 {
|
| 78 |
+
font-size: 22px;
|
| 79 |
+
font-weight: 700;
|
| 80 |
+
margin-top: 12px;
|
| 81 |
+
background: linear-gradient(135deg, var(--accent), var(--accent2));
|
| 82 |
+
-webkit-background-clip: text;
|
| 83 |
+
-webkit-text-fill-color: transparent
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
.login-logo p {
|
| 87 |
+
font-size: 13px;
|
| 88 |
+
color: var(--text3);
|
| 89 |
+
margin-top: 4px
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
.form-group {
|
| 93 |
+
margin-bottom: 16px
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
.form-group label {
|
| 97 |
+
display: block;
|
| 98 |
+
font-size: 12px;
|
| 99 |
+
font-weight: 600;
|
| 100 |
+
color: var(--text2);
|
| 101 |
+
text-transform: uppercase;
|
| 102 |
+
letter-spacing: .06em;
|
| 103 |
+
margin-bottom: 6px
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
.form-group input {
|
| 107 |
+
width: 100%;
|
| 108 |
+
padding: 11px 14px;
|
| 109 |
+
background: var(--surface2);
|
| 110 |
+
border: 1px solid var(--border);
|
| 111 |
+
border-radius: 8px;
|
| 112 |
+
color: var(--text);
|
| 113 |
+
font-size: 14px;
|
| 114 |
+
font-family: inherit;
|
| 115 |
+
outline: none;
|
| 116 |
+
transition: border-color .2s;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
.form-group input:focus {
|
| 120 |
+
border-color: var(--accent)
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
.btn-login {
|
| 124 |
+
width: 100%;
|
| 125 |
+
padding: 12px;
|
| 126 |
+
border: none;
|
| 127 |
+
border-radius: 8px;
|
| 128 |
+
cursor: pointer;
|
| 129 |
+
font-size: 14px;
|
| 130 |
+
font-weight: 600;
|
| 131 |
+
font-family: inherit;
|
| 132 |
+
background: linear-gradient(135deg, var(--accent), var(--accent2));
|
| 133 |
+
color: #fff;
|
| 134 |
+
transition: opacity .2s, transform .15s;
|
| 135 |
+
margin-top: 4px;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
.btn-login:hover {
|
| 139 |
+
opacity: .9;
|
| 140 |
+
transform: translateY(-1px)
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
.login-error {
|
| 144 |
+
display: none;
|
| 145 |
+
background: rgba(239, 68, 68, .12);
|
| 146 |
+
border: 1px solid rgba(239, 68, 68, .3);
|
| 147 |
+
border-radius: 8px;
|
| 148 |
+
padding: 10px 14px;
|
| 149 |
+
font-size: 13px;
|
| 150 |
+
color: #fca5a5;
|
| 151 |
+
margin-top: 14px;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
/* ── App Shell ── */
|
| 155 |
+
#app {
|
| 156 |
+
display: none;
|
| 157 |
+
height: 100vh;
|
| 158 |
+
display: grid;
|
| 159 |
+
grid-template-columns: 280px 1fr;
|
| 160 |
+
grid-template-rows: 60px 1fr
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
.topbar {
|
| 164 |
+
grid-column: 1/-1;
|
| 165 |
+
display: flex;
|
| 166 |
+
align-items: center;
|
| 167 |
+
justify-content: space-between;
|
| 168 |
+
padding: 0 24px;
|
| 169 |
+
background: var(--surface);
|
| 170 |
+
border-bottom: 1px solid var(--border);
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
.topbar-brand {
|
| 174 |
+
display: flex;
|
| 175 |
+
align-items: center;
|
| 176 |
+
gap: 10px;
|
| 177 |
+
font-weight: 700;
|
| 178 |
+
font-size: 16px
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
.topbar-brand .dot {
|
| 182 |
+
width: 8px;
|
| 183 |
+
height: 8px;
|
| 184 |
+
background: var(--green);
|
| 185 |
+
border-radius: 50%;
|
| 186 |
+
box-shadow: 0 0 8px var(--green);
|
| 187 |
+
animation: pulse 2s infinite
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
.topbar-right {
|
| 191 |
+
display: flex;
|
| 192 |
+
align-items: center;
|
| 193 |
+
gap: 14px
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
.badge {
|
| 197 |
+
font-size: 11px;
|
| 198 |
+
padding: 3px 10px;
|
| 199 |
+
border-radius: 20px;
|
| 200 |
+
background: rgba(59, 130, 246, .15);
|
| 201 |
+
color: var(--accent);
|
| 202 |
+
border: 1px solid rgba(59, 130, 246, .25)
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
.btn-logout {
|
| 206 |
+
padding: 7px 16px;
|
| 207 |
+
border: 1px solid var(--border);
|
| 208 |
+
border-radius: 8px;
|
| 209 |
+
background: transparent;
|
| 210 |
+
color: var(--text2);
|
| 211 |
+
font-size: 13px;
|
| 212 |
+
font-family: inherit;
|
| 213 |
+
cursor: pointer;
|
| 214 |
+
transition: all .2s;
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
.btn-logout:hover {
|
| 218 |
+
border-color: var(--red);
|
| 219 |
+
color: var(--red)
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
/* ── Sidebar ── */
|
| 223 |
+
.sidebar {
|
| 224 |
+
background: var(--surface);
|
| 225 |
+
border-right: 1px solid var(--border);
|
| 226 |
+
overflow: hidden;
|
| 227 |
+
display: flex;
|
| 228 |
+
flex-direction: column;
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
.sidebar-head {
|
| 232 |
+
padding: 16px 20px;
|
| 233 |
+
border-bottom: 1px solid var(--border);
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
.sidebar-head h2 {
|
| 237 |
+
font-size: 12px;
|
| 238 |
+
font-weight: 600;
|
| 239 |
+
text-transform: uppercase;
|
| 240 |
+
letter-spacing: .08em;
|
| 241 |
+
color: var(--text3)
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
.sidebar-stats {
|
| 245 |
+
display: flex;
|
| 246 |
+
gap: 16px;
|
| 247 |
+
margin-top: 10px
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
.stat {
|
| 251 |
+
text-align: center;
|
| 252 |
+
flex: 1;
|
| 253 |
+
background: var(--surface2);
|
| 254 |
+
border-radius: 8px;
|
| 255 |
+
padding: 8px 4px
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
.stat-val {
|
| 259 |
+
font-size: 20px;
|
| 260 |
+
font-weight: 700;
|
| 261 |
+
color: var(--accent)
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
.stat-lbl {
|
| 265 |
+
font-size: 10px;
|
| 266 |
+
color: var(--text3);
|
| 267 |
+
margin-top: 1px
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
.sidebar-actions {
|
| 271 |
+
padding: 12px 16px;
|
| 272 |
+
display: flex;
|
| 273 |
+
gap: 8px;
|
| 274 |
+
border-bottom: 1px solid var(--border)
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
.btn-export {
|
| 278 |
+
flex: 1;
|
| 279 |
+
padding: 8px;
|
| 280 |
+
border: 1px solid var(--border);
|
| 281 |
+
border-radius: 8px;
|
| 282 |
+
background: var(--surface2);
|
| 283 |
+
color: var(--text2);
|
| 284 |
+
font-size: 12px;
|
| 285 |
+
font-weight: 600;
|
| 286 |
+
cursor: pointer;
|
| 287 |
+
display: flex;
|
| 288 |
+
align-items: center;
|
| 289 |
+
justify-content: center;
|
| 290 |
+
gap: 6px;
|
| 291 |
+
transition: all .2s;
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
.btn-export:hover {
|
| 295 |
+
border-color: var(--accent);
|
| 296 |
+
color: var(--accent);
|
| 297 |
+
background: rgba(59, 130, 246, 0.05)
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
.export-dropdown {
|
| 301 |
+
position: relative;
|
| 302 |
+
display: inline-block;
|
| 303 |
+
flex: 1
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
.export-menu {
|
| 307 |
+
position: absolute;
|
| 308 |
+
top: 100%;
|
| 309 |
+
left: 0;
|
| 310 |
+
right: 0;
|
| 311 |
+
background: var(--surface3);
|
| 312 |
+
border: 1px solid var(--border);
|
| 313 |
+
border-radius: 8px;
|
| 314 |
+
margin-top: 4px;
|
| 315 |
+
display: none;
|
| 316 |
+
z-index: 100;
|
| 317 |
+
box-shadow: var(--shadow);
|
| 318 |
+
overflow: hidden;
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
.export-menu button {
|
| 322 |
+
width: 100%;
|
| 323 |
+
padding: 10px;
|
| 324 |
+
border: none;
|
| 325 |
+
background: none;
|
| 326 |
+
color: var(--text2);
|
| 327 |
+
font-size: 12px;
|
| 328 |
+
text-align: left;
|
| 329 |
+
cursor: pointer;
|
| 330 |
+
transition: all .2s;
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
.export-menu button:hover {
|
| 334 |
+
background: var(--surface2);
|
| 335 |
+
color: var(--text)
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
.export-dropdown:hover .export-menu {
|
| 339 |
+
display: block
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
.search-wrap {
|
| 343 |
+
padding: 12px 16px;
|
| 344 |
+
border-bottom: 1px solid var(--border)
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
.search-wrap input {
|
| 348 |
+
width: 100%;
|
| 349 |
+
padding: 8px 12px;
|
| 350 |
+
background: var(--surface2);
|
| 351 |
+
border: 1px solid var(--border);
|
| 352 |
+
border-radius: 8px;
|
| 353 |
+
color: var(--text);
|
| 354 |
+
font-size: 13px;
|
| 355 |
+
font-family: inherit;
|
| 356 |
+
outline: none;
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
.search-wrap input:focus {
|
| 360 |
+
border-color: var(--accent)
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
.session-list {
|
| 364 |
+
flex: 1;
|
| 365 |
+
overflow-y: auto;
|
| 366 |
+
padding: 8px
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
.session-list::-webkit-scrollbar {
|
| 370 |
+
width: 4px
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
.session-list::-webkit-scrollbar-track {
|
| 374 |
+
background: transparent
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
.session-list::-webkit-scrollbar-thumb {
|
| 378 |
+
background: var(--border);
|
| 379 |
+
border-radius: 4px
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
.session-item {
|
| 383 |
+
padding: 12px 14px;
|
| 384 |
+
border-radius: 10px;
|
| 385 |
+
cursor: pointer;
|
| 386 |
+
margin-bottom: 4px;
|
| 387 |
+
border: 1px solid transparent;
|
| 388 |
+
transition: all .2s;
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
.session-item:hover {
|
| 392 |
+
background: var(--surface2);
|
| 393 |
+
border-color: var(--border)
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
.session-item.active {
|
| 397 |
+
background: rgba(59, 130, 246, .1);
|
| 398 |
+
border-color: rgba(59, 130, 246, .35)
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
.session-item-id {
|
| 402 |
+
font-size: 12px;
|
| 403 |
+
font-weight: 600;
|
| 404 |
+
color: var(--text);
|
| 405 |
+
white-space: nowrap;
|
| 406 |
+
overflow: hidden;
|
| 407 |
+
text-overflow: ellipsis
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
.session-item-meta {
|
| 411 |
+
display: flex;
|
| 412 |
+
justify-content: space-between;
|
| 413 |
+
margin-top: 4px
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
.session-item-time {
|
| 417 |
+
font-size: 11px;
|
| 418 |
+
color: var(--text3)
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
.session-item-count {
|
| 422 |
+
font-size: 11px;
|
| 423 |
+
background: var(--surface3);
|
| 424 |
+
padding: 1px 7px;
|
| 425 |
+
border-radius: 20px;
|
| 426 |
+
color: var(--text2)
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
.no-sessions {
|
| 430 |
+
text-align: center;
|
| 431 |
+
padding: 40px 20px;
|
| 432 |
+
color: var(--text3);
|
| 433 |
+
font-size: 13px
|
| 434 |
+
}
|
| 435 |
+
|
| 436 |
+
/* ── Main Content ── */
|
| 437 |
+
.main {
|
| 438 |
+
overflow: hidden;
|
| 439 |
+
display: flex;
|
| 440 |
+
flex-direction: column;
|
| 441 |
+
background: var(--bg)
|
| 442 |
+
}
|
| 443 |
+
|
| 444 |
+
.main-placeholder {
|
| 445 |
+
flex: 1;
|
| 446 |
+
display: flex;
|
| 447 |
+
flex-direction: column;
|
| 448 |
+
align-items: center;
|
| 449 |
+
justify-content: center;
|
| 450 |
+
color: var(--text3);
|
| 451 |
+
gap: 14px;
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
.main-placeholder svg {
|
| 455 |
+
width: 56px;
|
| 456 |
+
height: 56px;
|
| 457 |
+
opacity: .3
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
.main-placeholder p {
|
| 461 |
+
font-size: 14px
|
| 462 |
+
}
|
| 463 |
+
|
| 464 |
+
.chat-view {
|
| 465 |
+
flex: 1;
|
| 466 |
+
display: flex;
|
| 467 |
+
flex-direction: column;
|
| 468 |
+
overflow: hidden
|
| 469 |
+
}
|
| 470 |
+
|
| 471 |
+
.chat-header {
|
| 472 |
+
padding: 16px 24px;
|
| 473 |
+
background: var(--surface);
|
| 474 |
+
border-bottom: 1px solid var(--border);
|
| 475 |
+
display: flex;
|
| 476 |
+
align-items: center;
|
| 477 |
+
justify-content: space-between;
|
| 478 |
+
}
|
| 479 |
+
|
| 480 |
+
.chat-header-info h3 {
|
| 481 |
+
font-size: 14px;
|
| 482 |
+
font-weight: 600
|
| 483 |
+
}
|
| 484 |
+
|
| 485 |
+
.chat-header-info p {
|
| 486 |
+
font-size: 12px;
|
| 487 |
+
color: var(--text3);
|
| 488 |
+
margin-top: 2px
|
| 489 |
+
}
|
| 490 |
+
|
| 491 |
+
.chat-header-actions {
|
| 492 |
+
display: flex;
|
| 493 |
+
gap: 8px
|
| 494 |
+
}
|
| 495 |
+
|
| 496 |
+
.btn-del {
|
| 497 |
+
padding: 6px 14px;
|
| 498 |
+
border: 1px solid rgba(239, 68, 68, .3);
|
| 499 |
+
border-radius: 8px;
|
| 500 |
+
background: rgba(239, 68, 68, .08);
|
| 501 |
+
color: #fca5a5;
|
| 502 |
+
font-size: 12px;
|
| 503 |
+
font-family: inherit;
|
| 504 |
+
cursor: pointer;
|
| 505 |
+
transition: all .2s;
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
.btn-del:hover {
|
| 509 |
+
background: rgba(239, 68, 68, .2)
|
| 510 |
+
}
|
| 511 |
+
|
| 512 |
+
.messages-area {
|
| 513 |
+
flex: 1;
|
| 514 |
+
overflow-y: auto;
|
| 515 |
+
padding: 24px;
|
| 516 |
+
display: flex;
|
| 517 |
+
flex-direction: column;
|
| 518 |
+
gap: 16px
|
| 519 |
+
}
|
| 520 |
+
|
| 521 |
+
.messages-area::-webkit-scrollbar {
|
| 522 |
+
width: 4px
|
| 523 |
+
}
|
| 524 |
+
|
| 525 |
+
.messages-area::-webkit-scrollbar-thumb {
|
| 526 |
+
background: var(--border);
|
| 527 |
+
border-radius: 4px
|
| 528 |
+
}
|
| 529 |
+
|
| 530 |
+
.msg-block {
|
| 531 |
+
display: flex;
|
| 532 |
+
flex-direction: column;
|
| 533 |
+
gap: 8px
|
| 534 |
+
}
|
| 535 |
+
|
| 536 |
+
.msg-label {
|
| 537 |
+
font-size: 10px;
|
| 538 |
+
font-weight: 700;
|
| 539 |
+
text-transform: uppercase;
|
| 540 |
+
letter-spacing: .08em;
|
| 541 |
+
display: flex;
|
| 542 |
+
align-items: center;
|
| 543 |
+
gap: 6px
|
| 544 |
+
}
|
| 545 |
+
|
| 546 |
+
.msg-label.q {
|
| 547 |
+
color: var(--accent)
|
| 548 |
+
}
|
| 549 |
+
|
| 550 |
+
.msg-label.a {
|
| 551 |
+
color: var(--green)
|
| 552 |
+
}
|
| 553 |
+
|
| 554 |
+
.msg-bubble {
|
| 555 |
+
padding: 13px 16px;
|
| 556 |
+
border-radius: 10px;
|
| 557 |
+
font-size: 14px;
|
| 558 |
+
line-height: 1.65;
|
| 559 |
+
white-space: pre-wrap;
|
| 560 |
+
word-break: break-word;
|
| 561 |
+
max-width: 100%;
|
| 562 |
+
}
|
| 563 |
+
|
| 564 |
+
.msg-bubble.q {
|
| 565 |
+
background: rgba(59, 130, 246, .1);
|
| 566 |
+
border: 1px solid rgba(59, 130, 246, .2);
|
| 567 |
+
color: var(--text)
|
| 568 |
+
}
|
| 569 |
+
|
| 570 |
+
.msg-bubble.a {
|
| 571 |
+
background: rgba(16, 185, 129, .08);
|
| 572 |
+
border: 1px solid rgba(16, 185, 129, .2);
|
| 573 |
+
color: var(--text)
|
| 574 |
+
}
|
| 575 |
+
|
| 576 |
+
.msg-ts {
|
| 577 |
+
font-size: 11px;
|
| 578 |
+
color: var(--text3);
|
| 579 |
+
margin-top: 2px
|
| 580 |
+
}
|
| 581 |
+
|
| 582 |
+
.msg-num {
|
| 583 |
+
font-size: 11px;
|
| 584 |
+
background: var(--surface2);
|
| 585 |
+
padding: 1px 8px;
|
| 586 |
+
border-radius: 20px;
|
| 587 |
+
color: var(--text3)
|
| 588 |
+
}
|
| 589 |
+
|
| 590 |
+
.spinner {
|
| 591 |
+
width: 32px;
|
| 592 |
+
height: 32px;
|
| 593 |
+
border: 3px solid var(--border);
|
| 594 |
+
border-top-color: var(--accent);
|
| 595 |
+
border-radius: 50%;
|
| 596 |
+
animation: spin .7s linear infinite;
|
| 597 |
+
margin: auto;
|
| 598 |
+
}
|
| 599 |
+
|
| 600 |
+
.toast {
|
| 601 |
+
position: fixed;
|
| 602 |
+
bottom: 24px;
|
| 603 |
+
right: 24px;
|
| 604 |
+
padding: 12px 20px;
|
| 605 |
+
background: var(--surface2);
|
| 606 |
+
border: 1px solid var(--border);
|
| 607 |
+
border-radius: 10px;
|
| 608 |
+
font-size: 13px;
|
| 609 |
+
color: var(--text);
|
| 610 |
+
box-shadow: var(--shadow);
|
| 611 |
+
transform: translateY(20px);
|
| 612 |
+
opacity: 0;
|
| 613 |
+
transition: all .3s;
|
| 614 |
+
pointer-events: none;
|
| 615 |
+
z-index: 9998;
|
| 616 |
+
}
|
| 617 |
+
|
| 618 |
+
.toast.show {
|
| 619 |
+
transform: translateY(0);
|
| 620 |
+
opacity: 1
|
| 621 |
+
}
|
| 622 |
+
|
| 623 |
+
.toast.success {
|
| 624 |
+
border-color: rgba(16, 185, 129, .4);
|
| 625 |
+
color: #6ee7b7
|
| 626 |
+
}
|
| 627 |
+
|
| 628 |
+
.toast.error {
|
| 629 |
+
border-color: rgba(239, 68, 68, .4);
|
| 630 |
+
color: #fca5a5
|
| 631 |
+
}
|
| 632 |
+
|
| 633 |
+
/* ── Animations ── */
|
| 634 |
+
@keyframes fadeUp {
|
| 635 |
+
from {
|
| 636 |
+
opacity: 0;
|
| 637 |
+
transform: translateY(18px)
|
| 638 |
+
}
|
| 639 |
+
|
| 640 |
+
to {
|
| 641 |
+
opacity: 1;
|
| 642 |
+
transform: translateY(0)
|
| 643 |
+
}
|
| 644 |
+
}
|
| 645 |
+
|
| 646 |
+
@keyframes pulse {
|
| 647 |
+
|
| 648 |
+
0%,
|
| 649 |
+
100% {
|
| 650 |
+
opacity: 1
|
| 651 |
+
}
|
| 652 |
+
|
| 653 |
+
50% {
|
| 654 |
+
opacity: .4
|
| 655 |
+
}
|
| 656 |
+
}
|
| 657 |
+
|
| 658 |
+
@keyframes spin {
|
| 659 |
+
to {
|
| 660 |
+
transform: rotate(360deg)
|
| 661 |
+
}
|
| 662 |
+
}
|
| 663 |
+
</style>
|
| 664 |
+
</head>
|
| 665 |
+
|
| 666 |
+
<body>
|
| 667 |
+
|
| 668 |
+
<!-- LOGIN SCREEN -->
|
| 669 |
+
<div id="login-screen">
|
| 670 |
+
<div class="login-card">
|
| 671 |
+
<div class="login-logo">
|
| 672 |
+
<svg viewBox="0 0 52 52" fill="none">
|
| 673 |
+
<rect width="52" height="52" rx="14" fill="url(#lg)" />
|
| 674 |
+
<path d="M16 26h20M26 16v20" stroke="#fff" stroke-width="2.5" stroke-linecap="round" />
|
| 675 |
+
<defs>
|
| 676 |
+
<linearGradient id="lg" x1="0" y1="0" x2="52" y2="52">
|
| 677 |
+
<stop stop-color="#3b82f6" />
|
| 678 |
+
<stop offset="1" stop-color="#6366f1" />
|
| 679 |
+
</linearGradient>
|
| 680 |
+
</defs>
|
| 681 |
+
</svg>
|
| 682 |
+
<h1>Martechsol Admin</h1>
|
| 683 |
+
<p>Secure Chat Analytics Dashboard</p>
|
| 684 |
+
</div>
|
| 685 |
+
<!-- Stage 1: Username & Password -->
|
| 686 |
+
<div id="login-stage-1">
|
| 687 |
+
<div class="form-group">
|
| 688 |
+
<label>Username</label>
|
| 689 |
+
<input type="text" id="inp-user" placeholder="Enter username" autocomplete="username" />
|
| 690 |
+
</div>
|
| 691 |
+
<div class="form-group">
|
| 692 |
+
<label>Password</label>
|
| 693 |
+
<input type="password" id="inp-pass" placeholder="Enter password" autocomplete="current-password" />
|
| 694 |
+
</div>
|
| 695 |
+
<button class="btn-login" id="btn-login">Sign In</button>
|
| 696 |
+
</div>
|
| 697 |
+
|
| 698 |
+
<!-- Stage 2: OTP (Hidden by default) -->
|
| 699 |
+
<div id="login-stage-2" style="display:none">
|
| 700 |
+
<div class="form-group">
|
| 701 |
+
<label>One-Time Password (OTP)</label>
|
| 702 |
+
<input type="text" id="inp-otp" placeholder="6-digit code" maxlength="6"
|
| 703 |
+
style="text-align:center;letter-spacing:0.5em;font-size:18px;font-weight:700" />
|
| 704 |
+
<p id="otp-hint" style="font-size:11px;color:var(--text3);margin-top:8px;text-align:center">Code sent to your
|
| 705 |
+
email</p>
|
| 706 |
+
</div>
|
| 707 |
+
<button class="btn-login" id="btn-verify">Verify & Enter</button>
|
| 708 |
+
<button class="btn-logout" id="btn-back" style="width:100%;margin-top:10px;border:none">Back to Login</button>
|
| 709 |
+
</div>
|
| 710 |
+
|
| 711 |
+
<div class="login-error" id="login-error">Invalid credentials. Please try again.</div>
|
| 712 |
+
</div>
|
| 713 |
+
</div>
|
| 714 |
+
|
| 715 |
+
<!-- APP SHELL (hidden until login) -->
|
| 716 |
+
<div id="app" style="display:none">
|
| 717 |
+
<!-- Topbar -->
|
| 718 |
+
<header class="topbar">
|
| 719 |
+
<div class="topbar-brand">
|
| 720 |
+
<div class="dot"></div>
|
| 721 |
+
Martechsol Admin Panel
|
| 722 |
+
</div>
|
| 723 |
+
<div class="topbar-right">
|
| 724 |
+
<span class="badge" id="total-badge">Loading…</span>
|
| 725 |
+
<button class="btn-logout" id="btn-logout">Sign Out</button>
|
| 726 |
+
</div>
|
| 727 |
+
</header>
|
| 728 |
+
|
| 729 |
+
<!-- Sidebar -->
|
| 730 |
+
<aside class="sidebar">
|
| 731 |
+
<div class="sidebar-head">
|
| 732 |
+
<h2>Sessions</h2>
|
| 733 |
+
<div class="sidebar-stats">
|
| 734 |
+
<div class="stat">
|
| 735 |
+
<div class="stat-val" id="stat-total">—</div>
|
| 736 |
+
<div class="stat-lbl">Total</div>
|
| 737 |
+
</div>
|
| 738 |
+
<div class="stat">
|
| 739 |
+
<div class="stat-val" id="stat-msgs">—</div>
|
| 740 |
+
<div class="stat-lbl">Messages</div>
|
| 741 |
+
</div>
|
| 742 |
+
<div class="stat">
|
| 743 |
+
<div class="stat-val" id="stat-today">—</div>
|
| 744 |
+
<div class="stat-lbl">Today</div>
|
| 745 |
+
</div>
|
| 746 |
+
</div>
|
| 747 |
+
</div>
|
| 748 |
+
<div class="sidebar-actions">
|
| 749 |
+
<div class="export-dropdown">
|
| 750 |
+
<button class="btn-export">
|
| 751 |
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 752 |
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3" />
|
| 753 |
+
</svg>
|
| 754 |
+
Export All
|
| 755 |
+
</button>
|
| 756 |
+
<div class="export-menu">
|
| 757 |
+
<button id="btn-export-all-excel">Excel (.xlsx)</button>
|
| 758 |
+
<button id="btn-export-all-json">JSON (.json)</button>
|
| 759 |
+
</div>
|
| 760 |
+
</div>
|
| 761 |
+
</div>
|
| 762 |
+
<div class="search-wrap">
|
| 763 |
+
<input type="text" id="search-inp" placeholder="Search sessions…" />
|
| 764 |
+
</div>
|
| 765 |
+
<div class="session-list" id="session-list">
|
| 766 |
+
<div class="no-sessions">Loading sessions…</div>
|
| 767 |
+
</div>
|
| 768 |
+
</aside>
|
| 769 |
+
|
| 770 |
+
<!-- Main -->
|
| 771 |
+
<main class="main" id="main-area">
|
| 772 |
+
<div class="main-placeholder" id="placeholder">
|
| 773 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
| 774 |
+
<path
|
| 775 |
+
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
|
| 776 |
+
stroke-linecap="round" stroke-linejoin="round" />
|
| 777 |
+
</svg>
|
| 778 |
+
<p>Select a session from the sidebar to view the transcript</p>
|
| 779 |
+
</div>
|
| 780 |
+
<div class="chat-view" id="chat-view" style="display:none">
|
| 781 |
+
<div class="chat-header">
|
| 782 |
+
<div class="chat-header-info">
|
| 783 |
+
<h3 id="view-session-id">Session ID</h3>
|
| 784 |
+
<p id="view-session-user" style="font-weight:600;color:var(--accent);margin-top:2px"></p>
|
| 785 |
+
<p id="view-session-meta">Loading…</p>
|
| 786 |
+
</div>
|
| 787 |
+
<div class="chat-header-actions">
|
| 788 |
+
<div class="export-dropdown">
|
| 789 |
+
<button class="btn-export" style="padding: 6px 12px;">
|
| 790 |
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 791 |
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3" />
|
| 792 |
+
</svg>
|
| 793 |
+
Export Session
|
| 794 |
+
</button>
|
| 795 |
+
<div class="export-menu" style="right:0;left:auto;min-width:120px">
|
| 796 |
+
<button id="btn-export-session-excel">Excel (.xlsx)</button>
|
| 797 |
+
<button id="btn-export-session-json">JSON (.json)</button>
|
| 798 |
+
</div>
|
| 799 |
+
</div>
|
| 800 |
+
<button class="btn-del" id="btn-delete-session">Delete Session</button>
|
| 801 |
+
</div>
|
| 802 |
+
</div>
|
| 803 |
+
<div class="messages-area" id="messages-area"></div>
|
| 804 |
+
</div>
|
| 805 |
+
</main>
|
| 806 |
+
</div>
|
| 807 |
+
|
| 808 |
+
<div class="toast" id="toast"></div>
|
| 809 |
+
|
| 810 |
+
<script>
|
| 811 |
+
(function () {
|
| 812 |
+
const CREDS = { user: 'martech_admin', pass: 'martech_admin_303' };
|
| 813 |
+
let _creds = null;
|
| 814 |
+
let _allSessions = [];
|
| 815 |
+
let _activeId = null;
|
| 816 |
+
|
| 817 |
+
const STORAGE_KEY = 'martech_admin_creds';
|
| 818 |
+
|
| 819 |
+
// ── Helpers ──────────────────────────────────────────────
|
| 820 |
+
function $(id) { return document.getElementById(id) }
|
| 821 |
+
function authHeader() { return 'Basic ' + btoa(_creds.user + ':' + _creds.pass) }
|
| 822 |
+
|
| 823 |
+
function showToast(msg, type = '') {
|
| 824 |
+
const t = $('toast');
|
| 825 |
+
t.textContent = msg; t.className = 'toast ' + (type || '');
|
| 826 |
+
t.classList.add('show');
|
| 827 |
+
setTimeout(() => t.classList.remove('show'), 3000);
|
| 828 |
+
}
|
| 829 |
+
|
| 830 |
+
function fmtDate(iso) {
|
| 831 |
+
if (!iso) return '—';
|
| 832 |
+
const d = new Date(iso);
|
| 833 |
+
return d.toLocaleDateString() + ' ' + d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
|
| 834 |
+
}
|
| 835 |
+
|
| 836 |
+
function shortId(id) {
|
| 837 |
+
if (!id) return '—';
|
| 838 |
+
return id.length > 16 ? id.slice(0, 8) + '…' + id.slice(-6) : id;
|
| 839 |
+
}
|
| 840 |
+
|
| 841 |
+
function todayCount(sessions) {
|
| 842 |
+
const today = new Date().toDateString();
|
| 843 |
+
return sessions.filter(s => {
|
| 844 |
+
try { return new Date(s.created_at).toDateString() === today } catch { return false }
|
| 845 |
+
}).length;
|
| 846 |
+
}
|
| 847 |
+
|
| 848 |
+
// ── Login ─────────────────────────────────────────────────
|
| 849 |
+
async function doLogin() {
|
| 850 |
+
const u = $('inp-user').value.trim();
|
| 851 |
+
const p = $('inp-pass').value;
|
| 852 |
+
if (!u || !p) return;
|
| 853 |
+
|
| 854 |
+
$('btn-login').textContent = 'Authenticating…';
|
| 855 |
+
$('btn-login').disabled = true;
|
| 856 |
+
$('login-error').style.display = 'none';
|
| 857 |
+
|
| 858 |
+
try {
|
| 859 |
+
const r = await fetch('/api/admin/request-otp', {
|
| 860 |
+
method: 'POST',
|
| 861 |
+
headers: { 'Content-Type': 'application/json' },
|
| 862 |
+
body: JSON.stringify({ username: u, password: p })
|
| 863 |
+
});
|
| 864 |
+
|
| 865 |
+
if (r.ok) {
|
| 866 |
+
const data = await r.json();
|
| 867 |
+
// Switch to OTP stage
|
| 868 |
+
$('login-stage-1').style.display = 'none';
|
| 869 |
+
$('login-stage-2').style.display = 'block';
|
| 870 |
+
if (data.email_status === 'logged_to_console_only') {
|
| 871 |
+
$('otp-hint').textContent = 'SMTP not configured. Check server logs.';
|
| 872 |
+
$('otp-hint').style.color = 'var(--yellow)';
|
| 873 |
+
} else {
|
| 874 |
+
$('otp-hint').textContent = 'Code sent to your registered email.';
|
| 875 |
+
$('otp-hint').style.color = 'var(--text3)';
|
| 876 |
+
}
|
| 877 |
+
$('inp-otp').focus();
|
| 878 |
+
} else {
|
| 879 |
+
const err = await r.json();
|
| 880 |
+
$('login-error').textContent = err.detail || 'Login failed.';
|
| 881 |
+
$('login-error').style.display = 'block';
|
| 882 |
+
}
|
| 883 |
+
} catch (e) {
|
| 884 |
+
$('login-error').textContent = 'Network error: ' + e.message;
|
| 885 |
+
$('login-error').style.display = 'block';
|
| 886 |
+
} finally {
|
| 887 |
+
$('btn-login').textContent = 'Sign In';
|
| 888 |
+
$('btn-login').disabled = false;
|
| 889 |
+
}
|
| 890 |
+
}
|
| 891 |
+
|
| 892 |
+
async function doVerifyOtp() {
|
| 893 |
+
const u = $('inp-user').value.trim();
|
| 894 |
+
const p = $('inp-pass').value;
|
| 895 |
+
const otp = $('inp-otp').value.trim();
|
| 896 |
+
if (!otp) return;
|
| 897 |
+
|
| 898 |
+
$('btn-verify').textContent = 'Verifying…';
|
| 899 |
+
$('btn-verify').disabled = true;
|
| 900 |
+
$('login-error').style.display = 'none';
|
| 901 |
+
|
| 902 |
+
try {
|
| 903 |
+
const r = await fetch('/api/admin/verify-otp', {
|
| 904 |
+
method: 'POST',
|
| 905 |
+
headers: { 'Content-Type': 'application/json' },
|
| 906 |
+
body: JSON.stringify({ username: u, password: p, code: otp })
|
| 907 |
+
});
|
| 908 |
+
|
| 909 |
+
if (r.ok) {
|
| 910 |
+
_creds = { user: u, pass: p, otp_verified: true };
|
| 911 |
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(_creds));
|
| 912 |
+
$('login-screen').style.display = 'none';
|
| 913 |
+
$('app').style.display = 'grid';
|
| 914 |
+
loadSessions();
|
| 915 |
+
showToast('Login successful', 'success');
|
| 916 |
+
} else {
|
| 917 |
+
const err = await r.json();
|
| 918 |
+
$('login-error').textContent = err.detail || 'Invalid OTP.';
|
| 919 |
+
$('login-error').style.display = 'block';
|
| 920 |
+
$('inp-otp').value = '';
|
| 921 |
+
}
|
| 922 |
+
} catch (e) {
|
| 923 |
+
$('login-error').textContent = 'Network error: ' + e.message;
|
| 924 |
+
$('login-error').style.display = 'block';
|
| 925 |
+
} finally {
|
| 926 |
+
$('btn-verify').textContent = 'Verify & Enter';
|
| 927 |
+
$('btn-verify').disabled = false;
|
| 928 |
+
}
|
| 929 |
+
}
|
| 930 |
+
|
| 931 |
+
$('btn-login').addEventListener('click', doLogin);
|
| 932 |
+
$('btn-verify').addEventListener('click', doVerifyOtp);
|
| 933 |
+
$('btn-back').addEventListener('click', () => {
|
| 934 |
+
$('login-stage-2').style.display = 'none';
|
| 935 |
+
$('login-stage-1').style.display = 'block';
|
| 936 |
+
$('login-error').style.display = 'none';
|
| 937 |
+
$('inp-otp').value = '';
|
| 938 |
+
});
|
| 939 |
+
|
| 940 |
+
[$('inp-user'), $('inp-pass')].forEach(el => el.addEventListener('keydown', e => { if (e.key === 'Enter') doLogin() }));
|
| 941 |
+
$('inp-otp').addEventListener('keydown', e => { if (e.key === 'Enter') doVerifyOtp() });
|
| 942 |
+
|
| 943 |
+
$('btn-logout').addEventListener('click', () => {
|
| 944 |
+
_creds = null; _allSessions = []; _activeId = null;
|
| 945 |
+
localStorage.removeItem(STORAGE_KEY);
|
| 946 |
+
$('app').style.display = 'none';
|
| 947 |
+
$('login-screen').style.display = 'flex';
|
| 948 |
+
$('login-stage-2').style.display = 'none';
|
| 949 |
+
$('login-stage-1').style.display = 'block';
|
| 950 |
+
$('inp-pass').value = '';
|
| 951 |
+
$('inp-otp').value = '';
|
| 952 |
+
$('login-error').style.display = 'none';
|
| 953 |
+
});
|
| 954 |
+
|
| 955 |
+
// ── Auto-Login ────────────────────────────────────────────
|
| 956 |
+
(function init() {
|
| 957 |
+
const stored = localStorage.getItem(STORAGE_KEY);
|
| 958 |
+
if (stored) {
|
| 959 |
+
try {
|
| 960 |
+
const parsed = JSON.parse(stored);
|
| 961 |
+
if (parsed.user === CREDS.user && parsed.pass === CREDS.pass && parsed.otp_verified) {
|
| 962 |
+
_creds = parsed;
|
| 963 |
+
$('login-screen').style.display = 'none';
|
| 964 |
+
$('app').style.display = 'grid';
|
| 965 |
+
loadSessions();
|
| 966 |
+
} else {
|
| 967 |
+
// Force re-login if OTP was not verified
|
| 968 |
+
localStorage.removeItem(STORAGE_KEY);
|
| 969 |
+
}
|
| 970 |
+
} catch (e) {
|
| 971 |
+
localStorage.removeItem(STORAGE_KEY);
|
| 972 |
+
}
|
| 973 |
+
}
|
| 974 |
+
})();
|
| 975 |
+
|
| 976 |
+
// ── Sessions list ─────────────────────────────────────────
|
| 977 |
+
async function loadSessions() {
|
| 978 |
+
$('session-list').innerHTML = '<div class="no-sessions"><div class="spinner"></div></div>';
|
| 979 |
+
try {
|
| 980 |
+
const r = await fetch('/api/admin/sessions', { headers: { Authorization: authHeader() } });
|
| 981 |
+
if (!r.ok) throw new Error('HTTP ' + r.status);
|
| 982 |
+
const data = await r.json();
|
| 983 |
+
_allSessions = data.sessions || [];
|
| 984 |
+
renderSidebar(_allSessions);
|
| 985 |
+
updateStats(_allSessions);
|
| 986 |
+
} catch (e) {
|
| 987 |
+
$('session-list').innerHTML = '<div class="no-sessions">Failed to load sessions.</div>';
|
| 988 |
+
showToast('Could not fetch sessions: ' + e.message, 'error');
|
| 989 |
+
}
|
| 990 |
+
}
|
| 991 |
+
|
| 992 |
+
function updateStats(sessions) {
|
| 993 |
+
const totalMsgs = sessions.reduce((a, s) => a + (s.message_count || 0), 0);
|
| 994 |
+
$('stat-total').textContent = sessions.length;
|
| 995 |
+
$('stat-msgs').textContent = totalMsgs;
|
| 996 |
+
$('stat-today').textContent = todayCount(sessions);
|
| 997 |
+
$('total-badge').textContent = sessions.length + ' Sessions';
|
| 998 |
+
}
|
| 999 |
+
|
| 1000 |
+
function renderSidebar(sessions) {
|
| 1001 |
+
if (!sessions.length) {
|
| 1002 |
+
$('session-list').innerHTML = '<div class="no-sessions">No sessions yet.</div>';
|
| 1003 |
+
return;
|
| 1004 |
+
}
|
| 1005 |
+
$('session-list').innerHTML = sessions.map(s => `
|
| 1006 |
+
<div class="session-item${s.session_id === _activeId ? ' active' : ''}"
|
| 1007 |
+
data-id="${s.session_id}">
|
| 1008 |
+
<div class="session-item-id" title="${s.user_name || s.session_id}">${s.user_name || shortId(s.session_id)}</div>
|
| 1009 |
+
<div class="session-item-meta">
|
| 1010 |
+
<span class="session-item-time">${fmtDate(s.last_active)}</span>
|
| 1011 |
+
<span class="session-item-count">${s.message_count || 0} msg${s.message_count !== 1 ? 's' : ''}</span>
|
| 1012 |
+
</div>
|
| 1013 |
+
</div>
|
| 1014 |
+
`).join('');
|
| 1015 |
+
document.querySelectorAll('.session-item').forEach(el => {
|
| 1016 |
+
el.addEventListener('click', () => openSession(el.dataset.id));
|
| 1017 |
+
});
|
| 1018 |
+
}
|
| 1019 |
+
|
| 1020 |
+
$('search-inp').addEventListener('input', e => {
|
| 1021 |
+
const q = e.target.value.toLowerCase();
|
| 1022 |
+
const filtered = _allSessions.filter(s => s.session_id.toLowerCase().includes(q));
|
| 1023 |
+
renderSidebar(filtered);
|
| 1024 |
+
});
|
| 1025 |
+
|
| 1026 |
+
// ── Session detail ────────────────────────────────────────
|
| 1027 |
+
async function openSession(id) {
|
| 1028 |
+
_activeId = id;
|
| 1029 |
+
document.querySelectorAll('.session-item').forEach(el => {
|
| 1030 |
+
el.classList.toggle('active', el.dataset.id === id);
|
| 1031 |
+
});
|
| 1032 |
+
$('placeholder').style.display = 'none';
|
| 1033 |
+
$('chat-view').style.display = 'flex';
|
| 1034 |
+
$('view-session-id').textContent = shortId(id);
|
| 1035 |
+
$('view-session-user').textContent = '';
|
| 1036 |
+
$('view-session-meta').textContent = 'Loading…';
|
| 1037 |
+
$('messages-area').innerHTML = '<div class="spinner" style="margin-top:60px"></div>';
|
| 1038 |
+
|
| 1039 |
+
try {
|
| 1040 |
+
const r = await fetch('/api/admin/sessions/' + encodeURIComponent(id),
|
| 1041 |
+
{ headers: { Authorization: authHeader() } });
|
| 1042 |
+
if (!r.ok) throw new Error('HTTP ' + r.status);
|
| 1043 |
+
const data = await r.json();
|
| 1044 |
+
renderSession(data);
|
| 1045 |
+
} catch (e) {
|
| 1046 |
+
$('messages-area').innerHTML = '<div style="color:var(--red);padding:20px">Failed to load session: ' + e.message + '</div>';
|
| 1047 |
+
}
|
| 1048 |
+
}
|
| 1049 |
+
|
| 1050 |
+
function renderSession(data) {
|
| 1051 |
+
$('view-session-user').textContent = data.user_name || 'Guest User';
|
| 1052 |
+
$('view-session-meta').textContent =
|
| 1053 |
+
'Created: ' + fmtDate(data.created_at) + ' | Last active: ' + fmtDate(data.last_active) +
|
| 1054 |
+
' | ' + (data.message_count || 0) + ' messages';
|
| 1055 |
+
|
| 1056 |
+
const msgs = data.messages || [];
|
| 1057 |
+
if (!msgs.length) {
|
| 1058 |
+
$('messages-area').innerHTML = '<div style="color:var(--text3);text-align:center;margin-top:40px">No messages in this session.</div>';
|
| 1059 |
+
return;
|
| 1060 |
+
}
|
| 1061 |
+
$('messages-area').innerHTML = msgs.map(m => `
|
| 1062 |
+
<div class="msg-block">
|
| 1063 |
+
<div style="display:flex;align-items:center;gap:8px">
|
| 1064 |
+
<span class="msg-label q">
|
| 1065 |
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z"/></svg>
|
| 1066 |
+
User Question
|
| 1067 |
+
</span>
|
| 1068 |
+
<span class="msg-num">#${m.id || '—'}</span>
|
| 1069 |
+
<span class="msg-ts">${fmtDate(m.timestamp)}</span>
|
| 1070 |
+
</div>
|
| 1071 |
+
<div class="msg-bubble q">${escHtml(m.question)}</div>
|
| 1072 |
+
<div class="msg-label a" style="margin-top:6px">
|
| 1073 |
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z"/></svg>
|
| 1074 |
+
Assistant Answer
|
| 1075 |
+
</div>
|
| 1076 |
+
<div class="msg-bubble a">${escHtml(m.answer)}</div>
|
| 1077 |
+
</div>
|
| 1078 |
+
`).join('');
|
| 1079 |
+
}
|
| 1080 |
+
|
| 1081 |
+
function escHtml(str) {
|
| 1082 |
+
return String(str || '')
|
| 1083 |
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
| 1084 |
+
.replace(/"/g, '"');
|
| 1085 |
+
}
|
| 1086 |
+
|
| 1087 |
+
// ── Delete session ────────────────────────────────────────
|
| 1088 |
+
$('btn-delete-session').addEventListener('click', async () => {
|
| 1089 |
+
if (!_activeId) return;
|
| 1090 |
+
if (!confirm('Delete session ' + shortId(_activeId) + '? This cannot be undone.')) return;
|
| 1091 |
+
try {
|
| 1092 |
+
const r = await fetch('/api/admin/sessions/' + encodeURIComponent(_activeId),
|
| 1093 |
+
{ method: 'DELETE', headers: { Authorization: authHeader() } });
|
| 1094 |
+
if (!r.ok) throw new Error('HTTP ' + r.status);
|
| 1095 |
+
showToast('Session deleted.', 'success');
|
| 1096 |
+
_allSessions = _allSessions.filter(s => s.session_id !== _activeId);
|
| 1097 |
+
_activeId = null;
|
| 1098 |
+
$('chat-view').style.display = 'none';
|
| 1099 |
+
$('placeholder').style.display = 'flex';
|
| 1100 |
+
renderSidebar(_allSessions);
|
| 1101 |
+
updateStats(_allSessions);
|
| 1102 |
+
} catch (e) { showToast('Delete failed: ' + e.message, 'error') }
|
| 1103 |
+
});
|
| 1104 |
+
|
| 1105 |
+
// ── Exports ───────────────────────────────────────────────
|
| 1106 |
+
async function downloadExport(url, filename) {
|
| 1107 |
+
try {
|
| 1108 |
+
const r = await fetch(url, { headers: { Authorization: authHeader() } });
|
| 1109 |
+
if (!r.ok) throw new Error('Export failed');
|
| 1110 |
+
const blob = await r.blob();
|
| 1111 |
+
const link = document.createElement('a');
|
| 1112 |
+
link.href = window.URL.createObjectURL(blob);
|
| 1113 |
+
link.download = filename;
|
| 1114 |
+
link.click();
|
| 1115 |
+
showToast('Export successful', 'success');
|
| 1116 |
+
} catch (e) {
|
| 1117 |
+
showToast('Export failed: ' + e.message, 'error');
|
| 1118 |
+
}
|
| 1119 |
+
}
|
| 1120 |
+
|
| 1121 |
+
$('btn-export-all-excel').addEventListener('click', () => {
|
| 1122 |
+
downloadExport('/api/admin/export/all?format=excel', 'all_chats_export.xlsx');
|
| 1123 |
+
});
|
| 1124 |
+
|
| 1125 |
+
$('btn-export-all-json').addEventListener('click', () => {
|
| 1126 |
+
downloadExport('/api/admin/export/all?format=json', 'all_chats_export.json');
|
| 1127 |
+
});
|
| 1128 |
+
|
| 1129 |
+
$('btn-export-session-excel').addEventListener('click', () => {
|
| 1130 |
+
if (!_activeId) return;
|
| 1131 |
+
downloadExport(`/api/admin/export/session/${encodeURIComponent(_activeId)}?format=excel`, `session_${_activeId}.xlsx`);
|
| 1132 |
+
});
|
| 1133 |
+
|
| 1134 |
+
$('btn-export-session-json').addEventListener('click', () => {
|
| 1135 |
+
if (!_activeId) return;
|
| 1136 |
+
downloadExport(`/api/admin/export/session/${encodeURIComponent(_activeId)}?format=json`, `session_${_activeId}.json`);
|
| 1137 |
+
});
|
| 1138 |
+
|
| 1139 |
+
})();
|
| 1140 |
+
</script>
|
| 1141 |
+
</body>
|
| 1142 |
+
|
| 1143 |
+
</html>
|
backup_2026-05-04/app/api/__init__.py
ADDED
|
File without changes
|
backup_2026-05-04/app/api/schemas.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Dict, Any
|
| 2 |
+
|
| 3 |
+
from pydantic import BaseModel, Field
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class ChatRequest(BaseModel):
|
| 7 |
+
message: str = Field(..., min_length=1)
|
| 8 |
+
history: List[Dict[str, str]] = Field(default_factory=list)
|
| 9 |
+
session_id: str | None = Field(default=None, description="Unique session identifier for chat persistence")
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class ChatResponse(BaseModel):
|
| 13 |
+
reply: str
|
| 14 |
+
retrieved_chunks: List[Dict[str, Any]] = Field(default_factory=list)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class HealthResponse(BaseModel):
|
| 18 |
+
status: str
|
| 19 |
+
docs_loaded: bool
|
| 20 |
+
index_ready: bool
|
| 21 |
+
chunk_count: int
|
| 22 |
+
index_path: str
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class SessionUpdateNameRequest(BaseModel):
|
| 26 |
+
session_id: str
|
| 27 |
+
user_name: str
|
backup_2026-05-04/app/core/__init__.py
ADDED
|
File without changes
|
backup_2026-05-04/app/core/config.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from functools import lru_cache
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
from pydantic import Field, model_validator
|
| 4 |
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class Settings(BaseSettings):
|
| 8 |
+
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
|
| 9 |
+
|
| 10 |
+
app_name: str = "Fast RAG Chatbot"
|
| 11 |
+
llm_provider: str = Field(default="groq", alias="LLM_PROVIDER")
|
| 12 |
+
groq_api_key: str = Field(default="", alias="GROQ_API_KEY")
|
| 13 |
+
groq_model: str = Field(default="qwen/qwen3-32b", alias="GROQ_MODEL")
|
| 14 |
+
groq_rewrite_model: str = Field(default="llama-3.1-8b-instant", alias="GROQ_REWRITE_MODEL")
|
| 15 |
+
hf_api_key: str = Field(default="", alias="HF_API_KEY")
|
| 16 |
+
hf_model: str = Field(default="meta-llama/Llama-3.1-8B-Instruct", alias="HF_MODEL")
|
| 17 |
+
embedding_model: str = Field(default="BAAI/bge-small-en-v1.5", alias="EMBEDDING_MODEL")
|
| 18 |
+
reranker_model: str = Field(default="BAAI/bge-reranker-base", alias="RERANKER_MODEL")
|
| 19 |
+
docs_dir: Path = Field(default=Path("docs"), alias="DOCS_DIR")
|
| 20 |
+
@model_validator(mode='after')
|
| 21 |
+
def set_persistent_paths(self) -> 'Settings':
|
| 22 |
+
# If we are on Hugging Face (detected by /data volume),
|
| 23 |
+
# force use of /data for persistence regardless of other settings.
|
| 24 |
+
if Path("/data").exists():
|
| 25 |
+
self.index_dir = Path("/data/index")
|
| 26 |
+
self.sessions_dir = Path("/data/sessions")
|
| 27 |
+
return self
|
| 28 |
+
|
| 29 |
+
index_dir: Path = Field(default=Path("data/index"), alias="INDEX_DIR")
|
| 30 |
+
sessions_dir: Path = Field(default=Path("data/sessions"), alias="SESSIONS_DIR")
|
| 31 |
+
chunk_size_tokens: int = 350 # Reduced for TPM safety
|
| 32 |
+
chunk_overlap_tokens: int = 80
|
| 33 |
+
top_k: int = Field(default=20, alias="TOP_K")
|
| 34 |
+
max_context_chunks: int = 10 # Increased to 10 to ensure all list items (like leaves) are captured
|
| 35 |
+
request_timeout_s: float = 20.0
|
| 36 |
+
cors_allow_origins: str = Field(default="*", alias="CORS_ALLOW_ORIGINS")
|
| 37 |
+
api_key: str = Field(default="", alias="API_KEY")
|
| 38 |
+
rate_limit_requests: int = Field(default=60, alias="RATE_LIMIT_REQUESTS")
|
| 39 |
+
rate_limit_window_seconds: int = Field(default=60, alias="RATE_LIMIT_WINDOW_SECONDS")
|
| 40 |
+
|
| 41 |
+
# SMTP Settings for OTP
|
| 42 |
+
smtp_server: str = Field(default="smtp.gmail.com", alias="SMTP_SERVER")
|
| 43 |
+
smtp_port: int = Field(default=465, alias="SMTP_PORT")
|
| 44 |
+
smtp_user: str = Field(default="", alias="SMTP_USER")
|
| 45 |
+
smtp_pass: str = Field(default="", alias="SMTP_PASS")
|
| 46 |
+
admin_email: str = Field(default="randomjoedown@gmail.com", alias="ADMIN_EMAIL")
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
@lru_cache
|
| 50 |
+
def get_settings() -> Settings:
|
| 51 |
+
return Settings()
|
backup_2026-05-04/app/main.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
|
| 3 |
+
from fastapi import FastAPI, HTTPException, Header, Request
|
| 4 |
+
from fastapi.responses import RedirectResponse
|
| 5 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 6 |
+
from fastapi.staticfiles import StaticFiles
|
| 7 |
+
import os
|
| 8 |
+
|
| 9 |
+
from app.api.schemas import ChatRequest, ChatResponse, HealthResponse, SessionUpdateNameRequest
|
| 10 |
+
from app.core.config import get_settings
|
| 11 |
+
from app.services.embeddings import EmbeddingService
|
| 12 |
+
from app.services.llm import LLMService
|
| 13 |
+
from app.services.rag_pipeline import RAGPipeline
|
| 14 |
+
from app.services.rate_limiter import InMemoryRateLimiter
|
| 15 |
+
from app.services.vector_store import FaissVectorStore
|
| 16 |
+
from app.services.reranker import RerankerService
|
| 17 |
+
from app.ui_gradio import demo
|
| 18 |
+
from app.admin.router import admin_router
|
| 19 |
+
from app.services import session_store as _ss
|
| 20 |
+
import asyncio
|
| 21 |
+
import gradio as gr
|
| 22 |
+
|
| 23 |
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s - %(message)s")
|
| 24 |
+
logger = logging.getLogger(__name__)
|
| 25 |
+
|
| 26 |
+
settings = get_settings()
|
| 27 |
+
embedding_service = EmbeddingService(settings.embedding_model)
|
| 28 |
+
vector_store = FaissVectorStore(
|
| 29 |
+
embedding_service=embedding_service,
|
| 30 |
+
docs_dir=settings.docs_dir,
|
| 31 |
+
index_dir=settings.index_dir,
|
| 32 |
+
chunk_size_tokens=settings.chunk_size_tokens,
|
| 33 |
+
chunk_overlap_tokens=settings.chunk_overlap_tokens,
|
| 34 |
+
)
|
| 35 |
+
llm_service = LLMService(
|
| 36 |
+
provider=settings.llm_provider,
|
| 37 |
+
groq_api_key=settings.groq_api_key,
|
| 38 |
+
groq_model=settings.groq_model,
|
| 39 |
+
groq_rewrite_model=settings.groq_rewrite_model,
|
| 40 |
+
hf_api_key=settings.hf_api_key,
|
| 41 |
+
hf_model=settings.hf_model,
|
| 42 |
+
timeout_s=settings.request_timeout_s,
|
| 43 |
+
)
|
| 44 |
+
reranker_service = RerankerService(settings.reranker_model)
|
| 45 |
+
pipeline = RAGPipeline(
|
| 46 |
+
vector_store=vector_store,
|
| 47 |
+
llm_service=llm_service,
|
| 48 |
+
reranker=reranker_service,
|
| 49 |
+
top_k=settings.top_k,
|
| 50 |
+
max_context_chunks=settings.max_context_chunks
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
# ── FastAPI App ───────────────────────────────────────────────────────────────
|
| 54 |
+
fastapi_app = FastAPI(title=settings.app_name)
|
| 55 |
+
|
| 56 |
+
allow_origins = [o.strip() for o in settings.cors_allow_origins.split(",") if o.strip()]
|
| 57 |
+
fastapi_app.add_middleware(
|
| 58 |
+
CORSMiddleware,
|
| 59 |
+
allow_origins=["*"], # Explicitly allow all for testing
|
| 60 |
+
allow_credentials=True,
|
| 61 |
+
allow_methods=["*"],
|
| 62 |
+
allow_headers=["*"],
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
rate_limiter = InMemoryRateLimiter(settings.rate_limit_requests, settings.rate_limit_window_seconds)
|
| 66 |
+
|
| 67 |
+
# ── Admin Panel (mounted on FastAPI before Gradio wrapping) ───────────────────
|
| 68 |
+
fastapi_app.include_router(admin_router)
|
| 69 |
+
|
| 70 |
+
# ── Static Files ──────────────────────────────────────────────────────────────
|
| 71 |
+
static_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "static")
|
| 72 |
+
if os.path.exists(static_dir):
|
| 73 |
+
fastapi_app.mount("/static", StaticFiles(directory=static_dir), name="static")
|
| 74 |
+
|
| 75 |
+
@fastapi_app.get("/widget", include_in_schema=False)
|
| 76 |
+
async def get_widget():
|
| 77 |
+
"""Serves the chat widget HTML with explicit CORS headers."""
|
| 78 |
+
from fastapi.responses import FileResponse
|
| 79 |
+
path = os.path.join(static_dir, "addon.html")
|
| 80 |
+
if not os.path.exists(path):
|
| 81 |
+
raise HTTPException(status_code=404, detail="Widget not found")
|
| 82 |
+
return FileResponse(
|
| 83 |
+
path,
|
| 84 |
+
headers={
|
| 85 |
+
"Access-Control-Allow-Origin": "*",
|
| 86 |
+
"Cache-Control": "no-cache, no-store, must-revalidate"
|
| 87 |
+
}
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
@fastapi_app.on_event("startup")
|
| 92 |
+
def startup_event() -> None:
|
| 93 |
+
logger.info("─── RAG SERVICE STARTUP ───")
|
| 94 |
+
logger.info("Index Directory: %s", settings.index_dir)
|
| 95 |
+
logger.info("Sessions Directory: %s", settings.sessions_dir)
|
| 96 |
+
|
| 97 |
+
# Verify persistence mount
|
| 98 |
+
if str(settings.index_dir).startswith("/data"):
|
| 99 |
+
if os.path.exists("/data"):
|
| 100 |
+
logger.info("✅ Persistent storage /data detected and in use.")
|
| 101 |
+
else:
|
| 102 |
+
logger.warning("⚠️ /data requested but NOT found on filesystem!")
|
| 103 |
+
else:
|
| 104 |
+
logger.warning("⚠️ Using ephemeral storage (data will be lost on rebuild).")
|
| 105 |
+
|
| 106 |
+
logger.info("Loading/building FAISS index...")
|
| 107 |
+
vector_store.build_or_load()
|
| 108 |
+
logger.info("RAG service started with %s chunks", len(vector_store.metadata))
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
@fastapi_app.get("/", include_in_schema=False)
|
| 112 |
+
async def root():
|
| 113 |
+
"""Prevent direct browser access to the root URL."""
|
| 114 |
+
raise HTTPException(
|
| 115 |
+
status_code=403,
|
| 116 |
+
detail="Direct access to this resource is restricted. Please use the authorized interface."
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
@fastapi_app.get("/health", response_model=HealthResponse)
|
| 121 |
+
def health() -> HealthResponse:
|
| 122 |
+
h = vector_store.health()
|
| 123 |
+
return HealthResponse(status="ok", **h)
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
@fastapi_app.post("/api/chat", response_model=ChatResponse)
|
| 127 |
+
async def chat(
|
| 128 |
+
payload: ChatRequest,
|
| 129 |
+
request: Request,
|
| 130 |
+
x_api_key: str | None = Header(default=None),
|
| 131 |
+
) -> ChatResponse:
|
| 132 |
+
if not payload.message.strip():
|
| 133 |
+
raise HTTPException(status_code=400, detail="message must not be empty")
|
| 134 |
+
if settings.api_key and x_api_key != settings.api_key:
|
| 135 |
+
raise HTTPException(status_code=401, detail="unauthorized")
|
| 136 |
+
|
| 137 |
+
client_ip = request.client.host if request.client else "unknown"
|
| 138 |
+
if not rate_limiter.allow(client_ip):
|
| 139 |
+
raise HTTPException(status_code=429, detail="rate limit exceeded")
|
| 140 |
+
|
| 141 |
+
logger.info("Incoming chat request: msg='%s', sid='%s'", payload.message[:30], payload.session_id)
|
| 142 |
+
|
| 143 |
+
# Fetch user_name if available for personalization
|
| 144 |
+
user_name = None
|
| 145 |
+
if payload.session_id:
|
| 146 |
+
sess_data = await _ss.get_session(payload.session_id)
|
| 147 |
+
user_name = sess_data.get("user_name")
|
| 148 |
+
|
| 149 |
+
try:
|
| 150 |
+
result = await pipeline.chat(payload.message, payload.history, user_name=user_name)
|
| 151 |
+
|
| 152 |
+
# ── Fire-and-forget: persist to session store in background ──
|
| 153 |
+
if payload.session_id:
|
| 154 |
+
asyncio.create_task(_ss.save_message(payload.session_id, payload.message, result["reply"]))
|
| 155 |
+
|
| 156 |
+
return ChatResponse(**result)
|
| 157 |
+
except Exception as e:
|
| 158 |
+
import httpx
|
| 159 |
+
import traceback
|
| 160 |
+
logger.error(f"Error in chat endpoint: {str(e)}\n{traceback.format_exc()}")
|
| 161 |
+
|
| 162 |
+
error_msg = "⚠️ Oops! Something went wrong."
|
| 163 |
+
if isinstance(e, httpx.HTTPStatusError):
|
| 164 |
+
if e.response.status_code == 429:
|
| 165 |
+
error_msg = "⚠️ Rate limit reached. Please slow down a bit!"
|
| 166 |
+
else:
|
| 167 |
+
error_msg = "⚠️ I encountered an error. Please try again in a few seconds."
|
| 168 |
+
|
| 169 |
+
return ChatResponse(reply=error_msg, retrieved_chunks=[])
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
@fastapi_app.post("/api/session/update-name")
|
| 173 |
+
async def update_session_user_name(payload: SessionUpdateNameRequest):
|
| 174 |
+
"""Updates the user name and renames the session file to match the name."""
|
| 175 |
+
new_id = await _ss.rename_session(payload.session_id, payload.user_name)
|
| 176 |
+
return {"status": "ok", "new_session_id": new_id}
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
@fastapi_app.get("/api/session/history/{session_id}")
|
| 180 |
+
async def get_session_history(session_id: str):
|
| 181 |
+
"""Returns the chat history for a session formatted for the frontend."""
|
| 182 |
+
data = await _ss.get_session(session_id)
|
| 183 |
+
if not data:
|
| 184 |
+
return {"history": []}
|
| 185 |
+
|
| 186 |
+
history = []
|
| 187 |
+
for msg in data.get("messages", []):
|
| 188 |
+
history.append({"role": "user", "content": msg["question"]})
|
| 189 |
+
history.append({"role": "assistant", "content": msg["answer"]})
|
| 190 |
+
return {"history": history, "user_name": data.get("user_name", "")}
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
# ── Mount Gradio at /ui — MUST be last, wraps the FastAPI app ─────────────────
|
| 194 |
+
# All FastAPI routes (including /admin) are already registered above and are
|
| 195 |
+
# preserved inside the Gradio-wrapped Starlette app.
|
| 196 |
+
app = gr.mount_gradio_app(fastapi_app, demo, path="/ui")
|
backup_2026-05-04/app/services/__init__.py
ADDED
|
File without changes
|
backup_2026-05-04/app/services/chunker.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Dict
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
def _tokenize(text: str) -> List[str]:
|
| 5 |
+
return text.split()
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def chunk_documents(
|
| 9 |
+
documents: List[Dict[str, str]],
|
| 10 |
+
chunk_size_tokens: int = 420,
|
| 11 |
+
chunk_overlap_tokens: int = 80,
|
| 12 |
+
) -> List[Dict[str, str]]:
|
| 13 |
+
chunks: List[Dict[str, str]] = []
|
| 14 |
+
step = max(1, chunk_size_tokens - chunk_overlap_tokens)
|
| 15 |
+
|
| 16 |
+
for doc in documents:
|
| 17 |
+
tokens = _tokenize(doc["text"])
|
| 18 |
+
if not tokens:
|
| 19 |
+
continue
|
| 20 |
+
|
| 21 |
+
i = 0
|
| 22 |
+
chunk_id = 0
|
| 23 |
+
while i < len(tokens):
|
| 24 |
+
window = tokens[i : i + chunk_size_tokens]
|
| 25 |
+
if not window:
|
| 26 |
+
break
|
| 27 |
+
|
| 28 |
+
chunk_text = " ".join(window)
|
| 29 |
+
chunks.append(
|
| 30 |
+
{
|
| 31 |
+
"id": f"{doc['source']}::chunk_{chunk_id}",
|
| 32 |
+
"source": doc["source"],
|
| 33 |
+
"text": chunk_text,
|
| 34 |
+
}
|
| 35 |
+
)
|
| 36 |
+
chunk_id += 1
|
| 37 |
+
i += step
|
| 38 |
+
|
| 39 |
+
return chunks
|
backup_2026-05-04/app/services/document_loader.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
from typing import List
|
| 3 |
+
|
| 4 |
+
from pypdf import PdfReader
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def _clean_text(text: str) -> str:
|
| 8 |
+
lines = [line.strip() for line in text.splitlines() if line.strip()]
|
| 9 |
+
merged = " ".join(lines)
|
| 10 |
+
return " ".join(merged.split())
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def load_documents(docs_dir: Path) -> List[dict]:
|
| 14 |
+
docs: List[dict] = []
|
| 15 |
+
if not docs_dir.exists():
|
| 16 |
+
return docs
|
| 17 |
+
|
| 18 |
+
for file_path in sorted(docs_dir.rglob("*")):
|
| 19 |
+
if not file_path.is_file():
|
| 20 |
+
continue
|
| 21 |
+
|
| 22 |
+
suffix = file_path.suffix.lower()
|
| 23 |
+
if suffix == ".txt":
|
| 24 |
+
raw = file_path.read_text(encoding="utf-8", errors="ignore")
|
| 25 |
+
cleaned = _clean_text(raw)
|
| 26 |
+
if cleaned:
|
| 27 |
+
docs.append({"source": str(file_path), "text": cleaned})
|
| 28 |
+
elif suffix == ".pdf":
|
| 29 |
+
reader = PdfReader(str(file_path))
|
| 30 |
+
pages = []
|
| 31 |
+
for page in reader.pages:
|
| 32 |
+
pages.append(page.extract_text() or "")
|
| 33 |
+
cleaned = _clean_text("\n".join(pages))
|
| 34 |
+
if cleaned:
|
| 35 |
+
docs.append({"source": str(file_path), "text": cleaned})
|
| 36 |
+
|
| 37 |
+
return docs
|
backup_2026-05-04/app/services/embeddings.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List
|
| 2 |
+
from functools import lru_cache
|
| 3 |
+
|
| 4 |
+
import numpy as np
|
| 5 |
+
from sentence_transformers import SentenceTransformer
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class EmbeddingService:
|
| 9 |
+
def __init__(self, model_name: str) -> None:
|
| 10 |
+
self.model = SentenceTransformer(model_name)
|
| 11 |
+
|
| 12 |
+
def encode(self, texts: List[str]) -> np.ndarray:
|
| 13 |
+
vectors = self.model.encode(
|
| 14 |
+
texts,
|
| 15 |
+
normalize_embeddings=True,
|
| 16 |
+
show_progress_bar=False,
|
| 17 |
+
convert_to_numpy=True,
|
| 18 |
+
)
|
| 19 |
+
return np.asarray(vectors, dtype=np.float32)
|
| 20 |
+
|
| 21 |
+
@lru_cache(maxsize=2048)
|
| 22 |
+
def encode_query_cached(self, text: str) -> bytes:
|
| 23 |
+
vec = self.model.encode(
|
| 24 |
+
[text],
|
| 25 |
+
normalize_embeddings=True,
|
| 26 |
+
show_progress_bar=False,
|
| 27 |
+
convert_to_numpy=True,
|
| 28 |
+
)
|
| 29 |
+
arr = np.asarray(vec, dtype=np.float32)
|
| 30 |
+
return arr.tobytes()
|
| 31 |
+
|
| 32 |
+
def encode_query(self, text: str) -> np.ndarray:
|
| 33 |
+
buf = self.encode_query_cached(text)
|
| 34 |
+
return np.frombuffer(buf, dtype=np.float32).reshape(1, -1)
|
backup_2026-05-04/app/services/llm.py
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Dict
|
| 2 |
+
import httpx
|
| 3 |
+
import re
|
| 4 |
+
|
| 5 |
+
# ═══════════════════════════════════════════════════════════════════════
|
| 6 |
+
# MASTER SYSTEM PROMPT — Martechsol HR Assistant
|
| 7 |
+
# Intelligence: Understand Intent → Retrieve Facts → Respond Precisely
|
| 8 |
+
# ═══════════════════════════════════════════════════════════════════════
|
| 9 |
+
SYSTEM_PROMPT = """You are the Martechsol HR Assistant — intelligent, precise, and formal.
|
| 10 |
+
|
| 11 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
| 12 |
+
STEP 1 — UNDERSTAND THE INTENT
|
| 13 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
| 14 |
+
Read the question carefully. Identify the SINGLE core topic being asked. Apply intelligent defaults:
|
| 15 |
+
• "timing" / "timings" (no context) → office working hours ONLY — not payment or any other timing
|
| 16 |
+
• "leaves" / "leave" (no context) → leave names + day counts ONLY — NOT leave policies or eligibility
|
| 17 |
+
• "paid leaves" / "all leaves" → enumerate EVERY leave type with its name and count
|
| 18 |
+
• "salary" / "pay" (no context) → salary structure or amount — NOT payment date unless explicitly asked
|
| 19 |
+
• "benefits" / "perks" / "allowances" → list EVERY benefit with its name and value
|
| 20 |
+
• "terminate" / "termination" → resignation/termination procedure — NOT general policies
|
| 21 |
+
If a question has an obvious workplace context, always default to the most common interpretation.
|
| 22 |
+
|
| 23 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
| 24 |
+
STEP 2 — STRICT SCOPE DISCIPLINE
|
| 25 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
| 26 |
+
Answer ONLY what was asked. NEVER expand into:
|
| 27 |
+
• Policies, approval processes, eligibility rules, or consequences — unless user asks for policy/process
|
| 28 |
+
• Related topics the user did not mention
|
| 29 |
+
• Broad overviews when a specific fact was requested
|
| 30 |
+
• Context that wasn't in the question
|
| 31 |
+
|
| 32 |
+
Scope examples:
|
| 33 |
+
"How many sick leaves?" → ONE number. Not the sick leave policy.
|
| 34 |
+
"What are office timings?" → ONE sentence with time range. Not break times or exceptions.
|
| 35 |
+
"What are paid leaves?" → Complete list of all paid leave types + counts. Not rules for taking them.
|
| 36 |
+
"Tell me about maternity leave" → Count + brief key fact. Not full policies unless asked.
|
| 37 |
+
|
| 38 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
| 39 |
+
STEP 3 — CHOOSE THE RIGHT FORMAT
|
| 40 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
| 41 |
+
|
| 42 |
+
FORMAT A — SINGLE FACT (default for most questions)
|
| 43 |
+
When: One direct answer needed (timings, a specific leave count, a single number/date)
|
| 44 |
+
Rule: ONE complete sentence. Maximum 25–30 words. Never cut mid-sentence.
|
| 45 |
+
Example: Office hours are <b>9:00 AM to 6:00 PM</b>, Monday to Saturday.
|
| 46 |
+
|
| 47 |
+
FORMAT B — EXHAUSTIVE LIST
|
| 48 |
+
When: User asks for ALL items in a category ("all leaves", "all benefits", "list all X", "paid leaves")
|
| 49 |
+
Rule:
|
| 50 |
+
• Include EVERY single item found — omitting even one is FORBIDDEN
|
| 51 |
+
• One item per line: <b>Item Name:</b> value<br>
|
| 52 |
+
• No intro sentence, no closing sentence, no extra commentary
|
| 53 |
+
• Continue until ALL items are listed
|
| 54 |
+
Example:
|
| 55 |
+
<b>Casual Leave:</b> 10 days<br>
|
| 56 |
+
<b>Sick Leave:</b> 10 days<br>
|
| 57 |
+
<b>Annual Leave:</b> 14 days<br>
|
| 58 |
+
<b>Maternity Leave:</b> 90 days<br>
|
| 59 |
+
<b>Paternity Leave:</b> 3 days<br>
|
| 60 |
+
<b>Hajj Leave:</b> 30 days<br>
|
| 61 |
+
...(list every item — do NOT stop early)
|
| 62 |
+
|
| 63 |
+
FORMAT C — BRIEF EXPLANATION
|
| 64 |
+
When: User asks HOW something works, or asks for a process/procedure
|
| 65 |
+
Rule: Maximum 3 bullet points. Each bullet = one complete, factual sentence. No filler words.
|
| 66 |
+
|
| 67 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
| 68 |
+
STRICT QUALITY RULES
|
| 69 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
| 70 |
+
✓ ZERO hallucination — every fact must exist in Expert Data only. No guessing.
|
| 71 |
+
✓ If not in Expert Data → reply exactly: "I don't have that information."
|
| 72 |
+
✓ Never cut a sentence mid-way — always complete every sentence fully
|
| 73 |
+
✓ NEVER mention: "document", "handbook", "manual", "policy file", or any source reference
|
| 74 |
+
✓ Use <b>bold</b> for names, numbers, dates, leave types, and all key terms
|
| 75 |
+
✓ Use <br> between list items for clean vertical spacing
|
| 76 |
+
✓ Tone: formal, warm, and professional — never robotic, never chatty
|
| 77 |
+
✓ Do NOT add greetings, closings, or "Is there anything else?" type phrases"""
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def _build_context(chunks: List[Dict[str, str]], max_words: int = 1500) -> str:
|
| 81 |
+
"""Combines retrieved chunks into a clean context string, capped at max_words.
|
| 82 |
+
Prevents TPM spikes when chunks are unexpectedly large."""
|
| 83 |
+
if not chunks:
|
| 84 |
+
return ""
|
| 85 |
+
parts = []
|
| 86 |
+
total_words = 0
|
| 87 |
+
for c in chunks:
|
| 88 |
+
text = c['text']
|
| 89 |
+
word_count = len(text.split())
|
| 90 |
+
if total_words + word_count > max_words:
|
| 91 |
+
# Add a trimmed version of this chunk if possible
|
| 92 |
+
remaining = max_words - total_words
|
| 93 |
+
if remaining > 30: # Only worth adding if meaningful content remains
|
| 94 |
+
trimmed_words = text.split()[:remaining]
|
| 95 |
+
parts.append(" ".join(trimmed_words))
|
| 96 |
+
break
|
| 97 |
+
parts.append(text)
|
| 98 |
+
total_words += word_count
|
| 99 |
+
return "\n\n".join(parts)
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
class LLMService:
|
| 103 |
+
def __init__(
|
| 104 |
+
self,
|
| 105 |
+
provider: str,
|
| 106 |
+
groq_api_key: str,
|
| 107 |
+
groq_model: str,
|
| 108 |
+
groq_rewrite_model: str,
|
| 109 |
+
hf_api_key: str,
|
| 110 |
+
hf_model: str,
|
| 111 |
+
timeout_s: float = 20.0,
|
| 112 |
+
) -> None:
|
| 113 |
+
self.provider = provider.lower().strip()
|
| 114 |
+
self.groq_api_key = groq_api_key
|
| 115 |
+
self.groq_model = groq_model
|
| 116 |
+
self.groq_rewrite_model = groq_rewrite_model
|
| 117 |
+
self.hf_api_key = hf_api_key
|
| 118 |
+
self.hf_model = hf_model
|
| 119 |
+
self.timeout_s = timeout_s
|
| 120 |
+
|
| 121 |
+
async def generate_multi_queries(self, query: str, history: List[Dict[str, str]]) -> List[str]:
|
| 122 |
+
"""Generates multiple search queries to capture broader context from the document."""
|
| 123 |
+
prompt = f"""You are an intelligent HR search optimizer. A user asked: "{query}"
|
| 124 |
+
|
| 125 |
+
STEP 1 — UNDERSTAND THE INTENT:
|
| 126 |
+
What is the user ACTUALLY asking about? Apply workplace common sense:
|
| 127 |
+
- "timing" / "timings" (no qualifier) = office working hours, workday schedule
|
| 128 |
+
- "leaves" / "paid leaves" / "all leaves" = all leave types available to employees
|
| 129 |
+
- "salary" / "pay" = salary structure or amount
|
| 130 |
+
- "benefits" / "allowances" = employee benefits and perks
|
| 131 |
+
- "terminate" / "resign" = termination or resignation process
|
| 132 |
+
|
| 133 |
+
STEP 2 — GENERATE 3 TARGETED SEARCH QUERIES:
|
| 134 |
+
Write 3 diverse search queries to retrieve ALL relevant information from an employee handbook.
|
| 135 |
+
For the identified intent, cover synonyms, related terms, and sub-categories:
|
| 136 |
+
- For office timings → search: working hours, office schedule, workday timing, shift hours
|
| 137 |
+
- For leaves → Query 1 MUST be a keyword dump: "casual leave sick leave annual leave maternity paternity hajj bereavement study unauthorized"
|
| 138 |
+
- For salary → cover: salary structure, payroll, compensation, increments, deductions
|
| 139 |
+
- For benefits → cover: allowances, perks, medical, bonuses, reimbursements
|
| 140 |
+
|
| 141 |
+
Output ONLY 3 queries, one per line. No numbers, no bullets, no explanations.
|
| 142 |
+
|
| 143 |
+
Queries:"""
|
| 144 |
+
|
| 145 |
+
try:
|
| 146 |
+
resp = await self._call_groq(
|
| 147 |
+
prompt, [],
|
| 148 |
+
"You output only search queries, one per line. No explanations, no numbering.",
|
| 149 |
+
model_override=self.groq_rewrite_model
|
| 150 |
+
)
|
| 151 |
+
queries = [
|
| 152 |
+
q.strip().lstrip('0123456789.-) ').strip()
|
| 153 |
+
for q in resp.split("\n")
|
| 154 |
+
if q.strip() and len(q.strip()) > 3
|
| 155 |
+
]
|
| 156 |
+
# Always include original query
|
| 157 |
+
if query not in queries:
|
| 158 |
+
queries.append(query)
|
| 159 |
+
return queries[:4]
|
| 160 |
+
except Exception:
|
| 161 |
+
return [query]
|
| 162 |
+
|
| 163 |
+
async def answer(
|
| 164 |
+
self,
|
| 165 |
+
question: str,
|
| 166 |
+
chunks: List[Dict[str, str]],
|
| 167 |
+
history: List[Dict[str, str]],
|
| 168 |
+
user_name: str = None
|
| 169 |
+
) -> str:
|
| 170 |
+
# Keep only last 4 messages (2 turns) for TPM safety
|
| 171 |
+
pruned_history = history[-4:]
|
| 172 |
+
context = _build_context(chunks)
|
| 173 |
+
|
| 174 |
+
# Build system message
|
| 175 |
+
system_msg = SYSTEM_PROMPT
|
| 176 |
+
if user_name and user_name not in ("default name", "", None):
|
| 177 |
+
system_msg += f"\n\nYou are speaking with {user_name}."
|
| 178 |
+
|
| 179 |
+
# Build user prompt — clean and direct
|
| 180 |
+
if not context:
|
| 181 |
+
user_prompt = question
|
| 182 |
+
else:
|
| 183 |
+
user_prompt = f"Expert Data:\n{context}\n\nQuestion: {question}"
|
| 184 |
+
|
| 185 |
+
return await self._call_groq(user_prompt, pruned_history, system_msg)
|
| 186 |
+
|
| 187 |
+
async def _call_groq(
|
| 188 |
+
self,
|
| 189 |
+
user_prompt: str,
|
| 190 |
+
history: List[Dict[str, str]],
|
| 191 |
+
system_msg: str,
|
| 192 |
+
model_override: str = None
|
| 193 |
+
) -> str:
|
| 194 |
+
if not self.groq_api_key:
|
| 195 |
+
return "Expert access required."
|
| 196 |
+
|
| 197 |
+
url = "https://api.groq.com/openai/v1/chat/completions"
|
| 198 |
+
headers = {
|
| 199 |
+
"Authorization": f"Bearer {self.groq_api_key}",
|
| 200 |
+
"Content-Type": "application/json"
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
messages = [{"role": "system", "content": system_msg}]
|
| 204 |
+
for msg in history:
|
| 205 |
+
messages.append({"role": msg["role"], "content": msg["content"]})
|
| 206 |
+
messages.append({"role": "user", "content": user_prompt})
|
| 207 |
+
|
| 208 |
+
target_model = model_override or self.groq_model
|
| 209 |
+
|
| 210 |
+
# Qwen3 and DeepSeek-R1 are thinking models
|
| 211 |
+
is_thinking_model = any(k in target_model.lower() for k in ["deepseek", "qwen3", "qwen/qwen3"])
|
| 212 |
+
temp = 0.6 if is_thinking_model else 0.0
|
| 213 |
+
|
| 214 |
+
# For Qwen3: suppress thinking mode on answer calls to reduce latency & TPM usage.
|
| 215 |
+
# /no_think is Qwen3's built-in signal to skip chain-of-thought reasoning.
|
| 216 |
+
# We ONLY suppress for the main answer model, not the rewrite model.
|
| 217 |
+
if is_thinking_model and model_override is None:
|
| 218 |
+
messages[-1]["content"] = "/no_think\n\n" + messages[-1]["content"]
|
| 219 |
+
|
| 220 |
+
payload = {
|
| 221 |
+
"model": target_model,
|
| 222 |
+
"temperature": temp,
|
| 223 |
+
"max_tokens": 512, # Enough for complete listings with HTML, well within TPM limits
|
| 224 |
+
"messages": messages,
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
async with httpx.AsyncClient(timeout=self.timeout_s) as client:
|
| 228 |
+
resp = await client.post(url, headers=headers, json=payload)
|
| 229 |
+
if resp.status_code >= 400:
|
| 230 |
+
import logging
|
| 231 |
+
logging.getLogger(__name__).error(
|
| 232 |
+
f"Groq API Error {resp.status_code}: {resp.text}"
|
| 233 |
+
)
|
| 234 |
+
resp.raise_for_status()
|
| 235 |
+
data = resp.json()
|
| 236 |
+
content = data["choices"][0]["message"]["content"].strip()
|
| 237 |
+
|
| 238 |
+
# ── Post-Processing: Strip all internal reasoning artifacts ──
|
| 239 |
+
|
| 240 |
+
# 1. Strip <think>...</think> blocks (Qwen3, DeepSeek-R1)
|
| 241 |
+
content = re.sub(r'<think>.*?</think>', '', content, flags=re.DOTALL).strip()
|
| 242 |
+
|
| 243 |
+
# 2. Strip leading conversational filler (single line only, not entire content)
|
| 244 |
+
content = re.sub(
|
| 245 |
+
r'^(Okay[,.]?\s*|Alright[,.]?\s*|Sure[,.]?\s*|Let\'s see[,.]?\s*|'
|
| 246 |
+
r'Based on (?:the )?(?:provided |available )?(?:data|information|context)[,.]?\s*|'
|
| 247 |
+
r'According to (?:the )?(?:provided |available )?(?:data|information)[,.]?\s*)',
|
| 248 |
+
'', content, flags=re.IGNORECASE
|
| 249 |
+
).strip()
|
| 250 |
+
|
| 251 |
+
# 3. Remove lines that are pure internal self-talk (only if they appear alone at start)
|
| 252 |
+
lines = content.split('\n')
|
| 253 |
+
filtered = []
|
| 254 |
+
for i, line in enumerate(lines):
|
| 255 |
+
is_self_talk = bool(re.match(
|
| 256 |
+
r'^\s*(I need to|I will|I should|I\'m going to|Let me|Now I|First,? I|'
|
| 257 |
+
r'I\'ll|The user is asking|The question is about)',
|
| 258 |
+
line, re.IGNORECASE
|
| 259 |
+
))
|
| 260 |
+
if not is_self_talk:
|
| 261 |
+
filtered.append(line)
|
| 262 |
+
content = '\n'.join(filtered).strip()
|
| 263 |
+
|
| 264 |
+
return content
|
backup_2026-05-04/app/services/rag_pipeline.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from typing import Dict, List
|
| 3 |
+
|
| 4 |
+
from app.services.llm import LLMService
|
| 5 |
+
from app.services.vector_store import FaissVectorStore
|
| 6 |
+
from app.services.reranker import RerankerService
|
| 7 |
+
|
| 8 |
+
logger = logging.getLogger(__name__)
|
| 9 |
+
|
| 10 |
+
# RRF scores are small (e.g. 0.016–0.033), so threshold must be very low
|
| 11 |
+
RELEVANCE_THRESHOLD = 0.01
|
| 12 |
+
# Cross-encoder logit > 0 means > 50% relevance probability
|
| 13 |
+
RERANK_THRESHOLD = 0.0
|
| 14 |
+
# If ALL chunks fail rerank threshold, fall back to this many top chunks
|
| 15 |
+
RERANK_FALLBACK_N = 2
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class RAGPipeline:
|
| 19 |
+
def __init__(
|
| 20 |
+
self,
|
| 21 |
+
vector_store: FaissVectorStore,
|
| 22 |
+
llm_service: LLMService,
|
| 23 |
+
reranker: RerankerService,
|
| 24 |
+
top_k: int,
|
| 25 |
+
max_context_chunks: int
|
| 26 |
+
) -> None:
|
| 27 |
+
self.vector_store = vector_store
|
| 28 |
+
self.llm_service = llm_service
|
| 29 |
+
self.reranker = reranker
|
| 30 |
+
self.top_k = top_k
|
| 31 |
+
self.max_context_chunks = max_context_chunks
|
| 32 |
+
|
| 33 |
+
async def chat(self, message: str, history: List[Dict[str, str]], user_name: str = None) -> Dict[str, object]:
|
| 34 |
+
# ── Step 1: Generate multiple search queries for broad coverage ──
|
| 35 |
+
queries = await self.llm_service.generate_multi_queries(message, history)
|
| 36 |
+
logger.info("Generated %d queries for: '%s' → %s", len(queries), message[:40], queries)
|
| 37 |
+
|
| 38 |
+
# ── Step 2: Collect unique chunks across all queries ──
|
| 39 |
+
seen_ids = set()
|
| 40 |
+
all_retrieved = []
|
| 41 |
+
|
| 42 |
+
for q in queries:
|
| 43 |
+
query_chunks = self.vector_store.search(q, top_k=self.top_k)
|
| 44 |
+
for chunk in query_chunks:
|
| 45 |
+
if chunk["id"] not in seen_ids:
|
| 46 |
+
all_retrieved.append(chunk)
|
| 47 |
+
seen_ids.add(chunk["id"])
|
| 48 |
+
|
| 49 |
+
# ── Step 3: Initial relevance filter ──
|
| 50 |
+
initial_chunks = [c for c in all_retrieved if c["score"] >= RELEVANCE_THRESHOLD]
|
| 51 |
+
|
| 52 |
+
if not initial_chunks:
|
| 53 |
+
logger.info("No relevant chunks found for: '%s' — returning no-info response", message)
|
| 54 |
+
# Pass empty chunks; LLM is instructed to say "I don't have that information"
|
| 55 |
+
reply = await self.llm_service.answer(
|
| 56 |
+
question=message,
|
| 57 |
+
chunks=[],
|
| 58 |
+
history=history,
|
| 59 |
+
user_name=user_name
|
| 60 |
+
)
|
| 61 |
+
return {"reply": reply, "retrieved_chunks": []}
|
| 62 |
+
|
| 63 |
+
# ── Step 4: Deep reranking via Cross-Encoder ──
|
| 64 |
+
# Enrihc the reranker query with the LLM's expanded search terms
|
| 65 |
+
rerank_query = message
|
| 66 |
+
if len(queries) > 0 and queries[0] != message:
|
| 67 |
+
rerank_query = f"{message} {queries[0]}"
|
| 68 |
+
|
| 69 |
+
reranked_chunks = self.reranker.rerank(rerank_query, initial_chunks, top_n=self.max_context_chunks)
|
| 70 |
+
|
| 71 |
+
# Filter by rerank score threshold
|
| 72 |
+
final_chunks = [c for c in reranked_chunks if c.get("rerank_score", 0) > RERANK_THRESHOLD]
|
| 73 |
+
|
| 74 |
+
# ── Smart Fallback: if ALL chunks fail the threshold, use the top N anyway ──
|
| 75 |
+
# This prevents the bot from saying "I don't have info" when content WAS retrieved
|
| 76 |
+
# but the cross-encoder wasn't confident enough (e.g. paraphrased queries)
|
| 77 |
+
if not final_chunks and reranked_chunks:
|
| 78 |
+
final_chunks = reranked_chunks[:RERANK_FALLBACK_N]
|
| 79 |
+
logger.info(
|
| 80 |
+
"Rerank fallback activated — all chunks below threshold (best score=%.3f), using top %d",
|
| 81 |
+
reranked_chunks[0].get("rerank_score", 0),
|
| 82 |
+
len(final_chunks)
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
logger.info(
|
| 86 |
+
"Pipeline: retrieved=%d → relevance_filtered=%d → reranked=%d → final=%d (best_score=%.3f)",
|
| 87 |
+
len(all_retrieved), len(initial_chunks), len(reranked_chunks), len(final_chunks),
|
| 88 |
+
reranked_chunks[0].get("rerank_score", 0) if reranked_chunks else 0.0
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
# ── Step 5: Generate answer with top-ranked context ──
|
| 92 |
+
reply = await self.llm_service.answer(
|
| 93 |
+
question=message,
|
| 94 |
+
chunks=final_chunks,
|
| 95 |
+
history=history,
|
| 96 |
+
user_name=user_name
|
| 97 |
+
)
|
| 98 |
+
return {"reply": reply, "retrieved_chunks": final_chunks}
|
backup_2026-05-04/app/services/rate_limiter.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import time
|
| 2 |
+
from collections import defaultdict, deque
|
| 3 |
+
from typing import Deque, Dict
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class InMemoryRateLimiter:
|
| 7 |
+
def __init__(self, max_requests: int, window_seconds: int) -> None:
|
| 8 |
+
self.max_requests = max_requests
|
| 9 |
+
self.window_seconds = window_seconds
|
| 10 |
+
self._hits: Dict[str, Deque[float]] = defaultdict(deque)
|
| 11 |
+
|
| 12 |
+
def allow(self, key: str) -> bool:
|
| 13 |
+
now = time.time()
|
| 14 |
+
window_start = now - self.window_seconds
|
| 15 |
+
q = self._hits[key]
|
| 16 |
+
|
| 17 |
+
while q and q[0] < window_start:
|
| 18 |
+
q.popleft()
|
| 19 |
+
|
| 20 |
+
if len(q) >= self.max_requests:
|
| 21 |
+
return False
|
| 22 |
+
|
| 23 |
+
q.append(now)
|
| 24 |
+
return True
|
backup_2026-05-04/app/services/reranker.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Dict
|
| 2 |
+
import numpy as np
|
| 3 |
+
from sentence_transformers import CrossEncoder
|
| 4 |
+
|
| 5 |
+
class RerankerService:
|
| 6 |
+
def __init__(self, model_name: str) -> None:
|
| 7 |
+
self.model = CrossEncoder(model_name)
|
| 8 |
+
|
| 9 |
+
def rerank(self, query: str, chunks: List[Dict[str, str]], top_n: int = 4) -> List[Dict[str, str]]:
|
| 10 |
+
if not chunks:
|
| 11 |
+
return []
|
| 12 |
+
|
| 13 |
+
# Prepare pairs for cross-encoder
|
| 14 |
+
pairs = [[query, chunk["text"]] for chunk in chunks]
|
| 15 |
+
|
| 16 |
+
# Get scores
|
| 17 |
+
scores = self.model.predict(pairs)
|
| 18 |
+
|
| 19 |
+
# Add scores to chunks and sort
|
| 20 |
+
for i, chunk in enumerate(chunks):
|
| 21 |
+
chunk["rerank_score"] = float(scores[i])
|
| 22 |
+
|
| 23 |
+
# Sort by rerank_score descending
|
| 24 |
+
sorted_chunks = sorted(chunks, key=lambda x: x["rerank_score"], reverse=True)
|
| 25 |
+
|
| 26 |
+
return sorted_chunks[:top_n]
|
backup_2026-05-04/app/services/session_store.py
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
session_store.py
|
| 3 |
+
────────────────
|
| 4 |
+
Async, fire-and-forget JSON-based chat session storage.
|
| 5 |
+
Each browser session gets its own JSON file in data/sessions/.
|
| 6 |
+
|
| 7 |
+
Rules:
|
| 8 |
+
• NEVER raises exceptions that propagate upward — any failure is silently logged.
|
| 9 |
+
• ALL disk I/O is done with asyncio to avoid blocking the event loop.
|
| 10 |
+
• Thread-safe: uses an asyncio.Lock per session_id to avoid race conditions.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import asyncio
|
| 14 |
+
import json
|
| 15 |
+
import logging
|
| 16 |
+
import os
|
| 17 |
+
from datetime import datetime, timezone
|
| 18 |
+
from pathlib import Path
|
| 19 |
+
from typing import Dict
|
| 20 |
+
|
| 21 |
+
from app.core.config import get_settings
|
| 22 |
+
logger = logging.getLogger(__name__)
|
| 23 |
+
settings = get_settings()
|
| 24 |
+
|
| 25 |
+
# ── Directory where session JSON files are stored ───────────────────────────
|
| 26 |
+
def _get_sessions_dir() -> Path:
|
| 27 |
+
"""Returns the current sessions directory from settings."""
|
| 28 |
+
return settings.sessions_dir
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
# ── Per-session locks to prevent concurrent write corruption ─────────────────
|
| 32 |
+
_session_locks: Dict[str, asyncio.Lock] = {}
|
| 33 |
+
_locks_meta_lock = asyncio.Lock() # protects the _session_locks dict itself
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def _get_session_path(session_id: str) -> Path:
|
| 37 |
+
"""Returns the absolute path for a session's JSON file."""
|
| 38 |
+
return _get_sessions_dir() / f"{session_id}.json"
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def _now_iso() -> str:
|
| 42 |
+
"""Returns the current UTC time as an ISO-8601 string."""
|
| 43 |
+
return datetime.now(timezone.utc).isoformat()
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
async def _get_lock(session_id: str) -> asyncio.Lock:
|
| 47 |
+
"""Gets or creates a per-session asyncio.Lock in a thread-safe manner."""
|
| 48 |
+
async with _locks_meta_lock:
|
| 49 |
+
if session_id not in _session_locks:
|
| 50 |
+
_session_locks[session_id] = asyncio.Lock()
|
| 51 |
+
return _session_locks[session_id]
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def _ensure_sessions_dir() -> None:
|
| 55 |
+
"""Creates the sessions directory if it does not exist. Sync, called once."""
|
| 56 |
+
try:
|
| 57 |
+
_get_sessions_dir().mkdir(parents=True, exist_ok=True)
|
| 58 |
+
except OSError as exc:
|
| 59 |
+
logger.error("session_store: cannot create sessions dir: %s", exc)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
# Call once at import time so the directory is always ready.
|
| 63 |
+
_ensure_sessions_dir()
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
async def ensure_session(session_id: str) -> None:
|
| 67 |
+
"""
|
| 68 |
+
Creates the session JSON file if it doesn't exist yet.
|
| 69 |
+
Safe to call on every page load — does nothing if file exists.
|
| 70 |
+
"""
|
| 71 |
+
path = _get_session_path(session_id)
|
| 72 |
+
if path.exists():
|
| 73 |
+
return
|
| 74 |
+
lock = await _get_lock(session_id)
|
| 75 |
+
async with lock:
|
| 76 |
+
# Double-check after acquiring lock
|
| 77 |
+
if path.exists():
|
| 78 |
+
return
|
| 79 |
+
skeleton = {
|
| 80 |
+
"session_id": session_id,
|
| 81 |
+
"user_name": "default name",
|
| 82 |
+
"created_at": _now_iso(),
|
| 83 |
+
"last_active": _now_iso(),
|
| 84 |
+
"message_count": 0,
|
| 85 |
+
"messages": [],
|
| 86 |
+
}
|
| 87 |
+
try:
|
| 88 |
+
await asyncio.to_thread(_write_json, path, skeleton)
|
| 89 |
+
logger.info("session_store: created session %s", session_id)
|
| 90 |
+
except Exception as exc:
|
| 91 |
+
logger.error("session_store: failed to create session %s: %s", session_id, exc)
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
async def save_message(session_id: str, question: str, answer: str) -> None:
|
| 95 |
+
"""
|
| 96 |
+
Appends a Question+Answer pair with timestamps to the session's JSON file.
|
| 97 |
+
Fire-and-forget safe — caller should wrap in asyncio.create_task().
|
| 98 |
+
"""
|
| 99 |
+
path = _get_session_path(session_id)
|
| 100 |
+
logger.info("session_store: save_message called for session_id=%s, path=%s", session_id, path)
|
| 101 |
+
lock = await _get_lock(session_id)
|
| 102 |
+
async with lock:
|
| 103 |
+
try:
|
| 104 |
+
# Load current data
|
| 105 |
+
if path.exists():
|
| 106 |
+
data = await asyncio.to_thread(_read_json, path)
|
| 107 |
+
else:
|
| 108 |
+
# Session file missing — recreate it gracefully
|
| 109 |
+
data = {
|
| 110 |
+
"session_id": session_id,
|
| 111 |
+
"user_name": "default name",
|
| 112 |
+
"created_at": _now_iso(),
|
| 113 |
+
"last_active": _now_iso(),
|
| 114 |
+
"message_count": 0,
|
| 115 |
+
"messages": [],
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
timestamp = _now_iso()
|
| 119 |
+
data["messages"].append({
|
| 120 |
+
"id": data["message_count"] + 1,
|
| 121 |
+
"timestamp": timestamp,
|
| 122 |
+
"question": question,
|
| 123 |
+
"answer": answer,
|
| 124 |
+
})
|
| 125 |
+
data["message_count"] += 1
|
| 126 |
+
data["last_active"] = timestamp
|
| 127 |
+
|
| 128 |
+
await asyncio.to_thread(_write_json, path, data)
|
| 129 |
+
logger.info("session_store: successfully saved message to %s", path)
|
| 130 |
+
except Exception as exc:
|
| 131 |
+
logger.error(
|
| 132 |
+
"session_store: failed to save message for session %s: %s",
|
| 133 |
+
session_id,
|
| 134 |
+
exc,
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
async def get_all_sessions() -> list:
|
| 139 |
+
"""
|
| 140 |
+
Returns a list of all session summaries (metadata only, no full messages).
|
| 141 |
+
Used by the Admin Panel sidebar.
|
| 142 |
+
"""
|
| 143 |
+
try:
|
| 144 |
+
files = await asyncio.to_thread(_list_json_files)
|
| 145 |
+
sessions = []
|
| 146 |
+
for fpath in files:
|
| 147 |
+
try:
|
| 148 |
+
data = await asyncio.to_thread(_read_json, fpath)
|
| 149 |
+
sessions.append({
|
| 150 |
+
"session_id": data.get("session_id", fpath.stem),
|
| 151 |
+
"user_name": data.get("user_name", ""),
|
| 152 |
+
"created_at": data.get("created_at", ""),
|
| 153 |
+
"last_active": data.get("last_active", ""),
|
| 154 |
+
"message_count": data.get("message_count", 0),
|
| 155 |
+
})
|
| 156 |
+
except Exception as exc:
|
| 157 |
+
logger.warning("session_store: could not read %s: %s", fpath, exc)
|
| 158 |
+
# Sort newest first
|
| 159 |
+
sessions.sort(key=lambda s: s.get("last_active", ""), reverse=True)
|
| 160 |
+
return sessions
|
| 161 |
+
except Exception as exc:
|
| 162 |
+
logger.error("session_store: get_all_sessions failed: %s", exc)
|
| 163 |
+
return []
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
async def get_session(session_id: str) -> dict:
|
| 167 |
+
"""
|
| 168 |
+
Returns the full session data (including all messages) for the Admin Panel.
|
| 169 |
+
Returns an empty dict if the session does not exist.
|
| 170 |
+
"""
|
| 171 |
+
path = _get_session_path(session_id)
|
| 172 |
+
try:
|
| 173 |
+
if not path.exists():
|
| 174 |
+
return {}
|
| 175 |
+
return await asyncio.to_thread(_read_json, path)
|
| 176 |
+
except Exception as exc:
|
| 177 |
+
logger.error("session_store: get_session(%s) failed: %s", session_id, exc)
|
| 178 |
+
return {}
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
async def delete_session(session_id: str) -> bool:
|
| 182 |
+
"""
|
| 183 |
+
Deletes a session file. Returns True on success, False on failure.
|
| 184 |
+
Used by Admin Panel if needed in the future.
|
| 185 |
+
"""
|
| 186 |
+
path = _get_session_path(session_id)
|
| 187 |
+
lock = await _get_lock(session_id)
|
| 188 |
+
async with lock:
|
| 189 |
+
try:
|
| 190 |
+
if path.exists():
|
| 191 |
+
await asyncio.to_thread(os.remove, path)
|
| 192 |
+
return True
|
| 193 |
+
except Exception as exc:
|
| 194 |
+
logger.error("session_store: delete_session(%s) failed: %s", session_id, exc)
|
| 195 |
+
return False
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
async def update_session_name(session_id: str, user_name: str) -> None:
|
| 199 |
+
"""Updates the user_name field in the session JSON file."""
|
| 200 |
+
path = _get_session_path(session_id)
|
| 201 |
+
lock = await _get_lock(session_id)
|
| 202 |
+
async with lock:
|
| 203 |
+
try:
|
| 204 |
+
if path.exists():
|
| 205 |
+
data = await asyncio.to_thread(_read_json, path)
|
| 206 |
+
data["user_name"] = user_name
|
| 207 |
+
await asyncio.to_thread(_write_json, path, data)
|
| 208 |
+
logger.info("session_store: updated name for session %s to %s", session_id, user_name)
|
| 209 |
+
else:
|
| 210 |
+
# If session doesn't exist yet, we can't update it, but we could create it?
|
| 211 |
+
# Usually ensure_session is called first.
|
| 212 |
+
logger.warning("session_store: attempt to update name for non-existent session %s", session_id)
|
| 213 |
+
except Exception as exc:
|
| 214 |
+
logger.error("session_store: failed to update name for session %s: %s", session_id, exc)
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
async def rename_session(old_id: str, new_name: str) -> str:
|
| 218 |
+
"""
|
| 219 |
+
Renames a session file from old_id to a name based on new_name.
|
| 220 |
+
Returns the new session_id used.
|
| 221 |
+
"""
|
| 222 |
+
import re
|
| 223 |
+
# 1. Sanitize the name for use as a filename
|
| 224 |
+
clean_name = re.sub(r'[^\w\s-]', '', new_name).strip().replace(' ', '_')
|
| 225 |
+
if not clean_name:
|
| 226 |
+
clean_name = "User"
|
| 227 |
+
|
| 228 |
+
new_id = clean_name
|
| 229 |
+
old_path = _get_session_path(old_id)
|
| 230 |
+
new_path = _get_session_path(new_id)
|
| 231 |
+
|
| 232 |
+
# Ensure uniqueness: if John_Doe exists, try John_Doe_1, etc.
|
| 233 |
+
counter = 1
|
| 234 |
+
while new_path.exists() and new_id != old_id:
|
| 235 |
+
new_id = f"{clean_name}_{counter}"
|
| 236 |
+
new_path = _get_session_path(new_id)
|
| 237 |
+
counter += 1
|
| 238 |
+
|
| 239 |
+
if new_id == old_id:
|
| 240 |
+
return old_id
|
| 241 |
+
|
| 242 |
+
# Lock both IDs to prevent race conditions during move
|
| 243 |
+
lock_old = await _get_lock(old_id)
|
| 244 |
+
lock_new = await _get_lock(new_id)
|
| 245 |
+
|
| 246 |
+
async with lock_old:
|
| 247 |
+
async with lock_new:
|
| 248 |
+
try:
|
| 249 |
+
if old_path.exists():
|
| 250 |
+
data = await asyncio.to_thread(_read_json, old_path)
|
| 251 |
+
data["session_id"] = new_id
|
| 252 |
+
data["user_name"] = new_name
|
| 253 |
+
|
| 254 |
+
# Write to new path and delete old
|
| 255 |
+
await asyncio.to_thread(_write_json, new_path, data)
|
| 256 |
+
await asyncio.to_thread(os.remove, old_path)
|
| 257 |
+
|
| 258 |
+
logger.info("session_store: renamed session %s -> %s", old_id, new_id)
|
| 259 |
+
return new_id
|
| 260 |
+
else:
|
| 261 |
+
logger.warning("session_store: cannot rename non-existent session %s", old_id)
|
| 262 |
+
return old_id
|
| 263 |
+
except Exception as exc:
|
| 264 |
+
logger.error("session_store: failed to rename session %s: %s", old_id, exc)
|
| 265 |
+
return old_id
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
# ── Sync helper functions (run via asyncio.to_thread) ────────────────────────
|
| 269 |
+
|
| 270 |
+
def _write_json(path: Path, data: dict) -> None:
|
| 271 |
+
"""Atomically writes JSON to a file using a temp file + rename pattern."""
|
| 272 |
+
tmp_path = path.with_suffix(".tmp")
|
| 273 |
+
with open(tmp_path, "w", encoding="utf-8") as f:
|
| 274 |
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
| 275 |
+
os.replace(tmp_path, path) # Atomic on POSIX, best-effort on Windows
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
def _read_json(path: Path) -> dict:
|
| 279 |
+
with open(path, "r", encoding="utf-8") as f:
|
| 280 |
+
return json.load(f)
|
| 281 |
+
|
| 282 |
+
|
| 283 |
+
def _list_json_files() -> list:
|
| 284 |
+
return list(_get_sessions_dir().glob("*.json"))
|
backup_2026-05-04/app/services/vector_store.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import hashlib
|
| 2 |
+
import json
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from typing import Dict, List, Tuple
|
| 5 |
+
|
| 6 |
+
import faiss
|
| 7 |
+
import numpy as np
|
| 8 |
+
import pickle
|
| 9 |
+
from rank_bm25 import BM25Okapi
|
| 10 |
+
|
| 11 |
+
from app.services.chunker import chunk_documents
|
| 12 |
+
from app.services.document_loader import load_documents
|
| 13 |
+
from app.services.embeddings import EmbeddingService
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class FaissVectorStore:
|
| 17 |
+
def __init__(
|
| 18 |
+
self,
|
| 19 |
+
embedding_service: EmbeddingService,
|
| 20 |
+
docs_dir: Path,
|
| 21 |
+
index_dir: Path,
|
| 22 |
+
chunk_size_tokens: int,
|
| 23 |
+
chunk_overlap_tokens: int,
|
| 24 |
+
) -> None:
|
| 25 |
+
self.embedding_service = embedding_service
|
| 26 |
+
self.docs_dir = docs_dir
|
| 27 |
+
self.index_dir = index_dir
|
| 28 |
+
self.chunk_size_tokens = chunk_size_tokens
|
| 29 |
+
self.chunk_overlap_tokens = chunk_overlap_tokens
|
| 30 |
+
|
| 31 |
+
self.faiss_index_path = index_dir / "faiss.index"
|
| 32 |
+
self.bm25_index_path = index_dir / "bm25.pkl"
|
| 33 |
+
self.metadata_path = index_dir / "metadata.json"
|
| 34 |
+
self.state_path = index_dir / "state.json"
|
| 35 |
+
|
| 36 |
+
self.index = None
|
| 37 |
+
self.bm25 = None
|
| 38 |
+
self.metadata: List[Dict[str, str]] = []
|
| 39 |
+
self.docs_loaded = False
|
| 40 |
+
self.last_retrieved: List[Dict[str, str]] = []
|
| 41 |
+
|
| 42 |
+
def _compute_docs_fingerprint(self) -> str:
|
| 43 |
+
hasher = hashlib.sha256()
|
| 44 |
+
# Include chunk settings in fingerprint so changing them triggers re-index
|
| 45 |
+
hasher.update(str(self.chunk_size_tokens).encode("utf-8"))
|
| 46 |
+
hasher.update(str(self.chunk_overlap_tokens).encode("utf-8"))
|
| 47 |
+
|
| 48 |
+
if not self.docs_dir.exists():
|
| 49 |
+
return "no_docs"
|
| 50 |
+
for path in sorted(self.docs_dir.rglob("*")):
|
| 51 |
+
if path.is_file() and path.suffix.lower() in {".txt", ".pdf"}:
|
| 52 |
+
stat = path.stat()
|
| 53 |
+
hasher.update(str(path).encode("utf-8"))
|
| 54 |
+
hasher.update(str(stat.st_mtime_ns).encode("utf-8"))
|
| 55 |
+
hasher.update(str(stat.st_size).encode("utf-8"))
|
| 56 |
+
return hasher.hexdigest()
|
| 57 |
+
|
| 58 |
+
def _can_use_cached_index(self, fingerprint: str) -> bool:
|
| 59 |
+
if not (self.faiss_index_path.exists() and self.metadata_path.exists() and self.state_path.exists()):
|
| 60 |
+
return False
|
| 61 |
+
try:
|
| 62 |
+
state = json.loads(self.state_path.read_text(encoding="utf-8"))
|
| 63 |
+
return state.get("docs_fingerprint") == fingerprint
|
| 64 |
+
except Exception:
|
| 65 |
+
return False
|
| 66 |
+
|
| 67 |
+
def build_or_load(self) -> None:
|
| 68 |
+
self.index_dir.mkdir(parents=True, exist_ok=True)
|
| 69 |
+
fingerprint = self._compute_docs_fingerprint()
|
| 70 |
+
|
| 71 |
+
if self._can_use_cached_index(fingerprint):
|
| 72 |
+
self.index = faiss.read_index(str(self.faiss_index_path))
|
| 73 |
+
with open(self.bm25_index_path, "rb") as f:
|
| 74 |
+
self.bm25 = pickle.load(f)
|
| 75 |
+
self.metadata = json.loads(self.metadata_path.read_text(encoding="utf-8"))
|
| 76 |
+
self.docs_loaded = len(self.metadata) > 0
|
| 77 |
+
return
|
| 78 |
+
|
| 79 |
+
docs = load_documents(self.docs_dir)
|
| 80 |
+
chunks = chunk_documents(docs, self.chunk_size_tokens, self.chunk_overlap_tokens)
|
| 81 |
+
if not chunks:
|
| 82 |
+
self.index = None
|
| 83 |
+
self.metadata = []
|
| 84 |
+
self.docs_loaded = False
|
| 85 |
+
return
|
| 86 |
+
|
| 87 |
+
vectors = self.embedding_service.encode([c["text"] for c in chunks])
|
| 88 |
+
dim = vectors.shape[1]
|
| 89 |
+
index = faiss.IndexFlatIP(dim)
|
| 90 |
+
index.add(vectors)
|
| 91 |
+
|
| 92 |
+
tokenized_corpus = [c["text"].lower().split() for c in chunks]
|
| 93 |
+
bm25 = BM25Okapi(tokenized_corpus)
|
| 94 |
+
|
| 95 |
+
self.index = index
|
| 96 |
+
self.bm25 = bm25
|
| 97 |
+
self.metadata = chunks
|
| 98 |
+
self.docs_loaded = True
|
| 99 |
+
|
| 100 |
+
faiss.write_index(index, str(self.faiss_index_path))
|
| 101 |
+
with open(self.bm25_index_path, "wb") as f:
|
| 102 |
+
pickle.dump(bm25, f)
|
| 103 |
+
self.metadata_path.write_text(json.dumps(chunks, ensure_ascii=False, indent=2), encoding="utf-8")
|
| 104 |
+
self.state_path.write_text(
|
| 105 |
+
json.dumps({"docs_fingerprint": fingerprint, "chunk_count": len(chunks)}, indent=2),
|
| 106 |
+
encoding="utf-8",
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
def search(self, query: str, top_k: int = 4) -> List[Dict[str, str]]:
|
| 110 |
+
if self.index is None or not self.metadata or self.bm25 is None:
|
| 111 |
+
self.last_retrieved = []
|
| 112 |
+
return []
|
| 113 |
+
|
| 114 |
+
# Vector Search (FAISS)
|
| 115 |
+
query_vec = self.embedding_service.encode_query(query)
|
| 116 |
+
faiss_scores, faiss_indices = self.index.search(np.asarray(query_vec, dtype=np.float32), top_k * 2)
|
| 117 |
+
|
| 118 |
+
# BM25 Search
|
| 119 |
+
tokenized_query = query.lower().split()
|
| 120 |
+
bm25_scores = self.bm25.get_scores(tokenized_query)
|
| 121 |
+
|
| 122 |
+
# Get top indices for BM25
|
| 123 |
+
bm25_top_indices = np.argsort(bm25_scores)[::-1][:top_k * 2]
|
| 124 |
+
|
| 125 |
+
# Combine using Reciprocal Rank Fusion (RRF)
|
| 126 |
+
# RRF_score = 1 / (k + rank)
|
| 127 |
+
k = 60
|
| 128 |
+
rrf_scores = {}
|
| 129 |
+
|
| 130 |
+
# Add FAISS ranks
|
| 131 |
+
for rank, idx in enumerate(faiss_indices[0]):
|
| 132 |
+
if idx < 0 or idx >= len(self.metadata):
|
| 133 |
+
continue
|
| 134 |
+
rrf_scores[idx] = rrf_scores.get(idx, 0.0) + (1.0 / (k + rank + 1))
|
| 135 |
+
|
| 136 |
+
# Add BM25 ranks
|
| 137 |
+
for rank, idx in enumerate(bm25_top_indices):
|
| 138 |
+
if idx < 0 or idx >= len(self.metadata):
|
| 139 |
+
continue
|
| 140 |
+
rrf_scores[idx] = rrf_scores.get(idx, 0.0) + (1.0 / (k + rank + 1))
|
| 141 |
+
|
| 142 |
+
# Sort by combined RRF score
|
| 143 |
+
sorted_indices = sorted(rrf_scores.keys(), key=lambda x: rrf_scores[x], reverse=True)[:top_k]
|
| 144 |
+
|
| 145 |
+
results: List[Dict[str, str]] = []
|
| 146 |
+
for idx in sorted_indices:
|
| 147 |
+
chunk = self.metadata[idx]
|
| 148 |
+
# Fetch the original FAISS score for fallback relevance checking if needed, or just use RRF score
|
| 149 |
+
# Note: RRF scores are small (e.g. 0.03), so we must adjust the threshold in rag_pipeline
|
| 150 |
+
results.append(
|
| 151 |
+
{
|
| 152 |
+
"id": chunk["id"],
|
| 153 |
+
"source": chunk["source"],
|
| 154 |
+
"text": chunk["text"],
|
| 155 |
+
"score": float(rrf_scores[idx]), # RRF score
|
| 156 |
+
}
|
| 157 |
+
)
|
| 158 |
+
self.last_retrieved = results
|
| 159 |
+
return results
|
| 160 |
+
|
| 161 |
+
def health(self) -> Dict[str, object]:
|
| 162 |
+
return {
|
| 163 |
+
"docs_loaded": self.docs_loaded,
|
| 164 |
+
"index_ready": self.index is not None,
|
| 165 |
+
"chunk_count": len(self.metadata),
|
| 166 |
+
"index_path": str(self.faiss_index_path),
|
| 167 |
+
}
|
backup_2026-05-04/app/ui_gradio.py
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import logging
|
| 3 |
+
import os
|
| 4 |
+
import random
|
| 5 |
+
import string
|
| 6 |
+
import traceback
|
| 7 |
+
import uuid
|
| 8 |
+
from typing import Dict, List, Tuple
|
| 9 |
+
|
| 10 |
+
from app.services import session_store as _ss
|
| 11 |
+
|
| 12 |
+
import gradio as gr
|
| 13 |
+
import httpx
|
| 14 |
+
|
| 15 |
+
from app.core.config import get_settings
|
| 16 |
+
from app.services.embeddings import EmbeddingService
|
| 17 |
+
from app.services.llm import LLMService
|
| 18 |
+
from app.services.rag_pipeline import RAGPipeline
|
| 19 |
+
from app.services.vector_store import FaissVectorStore
|
| 20 |
+
from app.services.reranker import RerankerService
|
| 21 |
+
|
| 22 |
+
logger = logging.getLogger(__name__)
|
| 23 |
+
settings = get_settings()
|
| 24 |
+
API_URL = os.getenv("RAG_API_URL", "").strip()
|
| 25 |
+
|
| 26 |
+
# Initialize services
|
| 27 |
+
embedding_service = EmbeddingService(settings.embedding_model)
|
| 28 |
+
vector_store = FaissVectorStore(
|
| 29 |
+
embedding_service=embedding_service,
|
| 30 |
+
docs_dir=settings.docs_dir,
|
| 31 |
+
index_dir=settings.index_dir,
|
| 32 |
+
chunk_size_tokens=settings.chunk_size_tokens,
|
| 33 |
+
chunk_overlap_tokens=settings.chunk_overlap_tokens,
|
| 34 |
+
)
|
| 35 |
+
llm_service = LLMService(
|
| 36 |
+
provider=settings.llm_provider,
|
| 37 |
+
groq_api_key=settings.groq_api_key,
|
| 38 |
+
groq_model=settings.groq_model,
|
| 39 |
+
groq_rewrite_model=settings.groq_rewrite_model,
|
| 40 |
+
hf_api_key=settings.hf_api_key,
|
| 41 |
+
hf_model=settings.hf_model,
|
| 42 |
+
timeout_s=settings.request_timeout_s,
|
| 43 |
+
)
|
| 44 |
+
reranker_service = RerankerService(settings.reranker_model)
|
| 45 |
+
pipeline = RAGPipeline(
|
| 46 |
+
vector_store=vector_store,
|
| 47 |
+
llm_service=llm_service,
|
| 48 |
+
reranker=reranker_service,
|
| 49 |
+
top_k=settings.top_k,
|
| 50 |
+
max_context_chunks=settings.max_context_chunks
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
# Ensure index is ready
|
| 54 |
+
vector_store.build_or_load()
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
# ── Keyboard layout for realistic typos ──────────────────────────────
|
| 58 |
+
NEARBY_KEYS = {
|
| 59 |
+
'a': 'sqwz', 'b': 'vghn', 'c': 'xdfv', 'd': 'sfecx', 'e': 'wrsdf',
|
| 60 |
+
'f': 'dgrtcv', 'g': 'fhtybn', 'h': 'gjyunb', 'i': 'uojkl', 'j': 'hkunmi',
|
| 61 |
+
'k': 'jlomi', 'l': 'kop', 'm': 'njk', 'n': 'bhjm', 'o': 'ipkl',
|
| 62 |
+
'p': 'ol', 'q': 'wa', 'r': 'edft', 's': 'awedxz', 't': 'rfgy',
|
| 63 |
+
'u': 'yihj', 'v': 'cfgb', 'w': 'qase', 'x': 'zsdc', 'y': 'tghu',
|
| 64 |
+
'z': 'xas',
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
TYPO_CHANCE = 0.07 # 7% chance per word
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def _nearby_char(ch: str) -> str:
|
| 71 |
+
"""Return a plausible neighbouring key for the given character."""
|
| 72 |
+
lower = ch.lower()
|
| 73 |
+
if lower in NEARBY_KEYS:
|
| 74 |
+
replacement = random.choice(NEARBY_KEYS[lower])
|
| 75 |
+
return replacement.upper() if ch.isupper() else replacement
|
| 76 |
+
return random.choice(string.ascii_lowercase)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def _to_history(messages: List[Dict[str, str]]) -> list:
|
| 80 |
+
"""Returns the history as a list of dicts (already in this format for Gradio 5)."""
|
| 81 |
+
return messages
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
async def _chat_via_api(message: str, history: List[Dict[str, str]]) -> str:
|
| 85 |
+
payload = {"message": message, "history": history}
|
| 86 |
+
headers = {"Content-Type": "application/json"}
|
| 87 |
+
if settings.api_key:
|
| 88 |
+
headers["x-api-key"] = settings.api_key
|
| 89 |
+
|
| 90 |
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
| 91 |
+
resp = await client.post(API_URL, json=payload, headers=headers)
|
| 92 |
+
if resp.status_code == 429:
|
| 93 |
+
return "⚠️ I'm receiving too many requests right now. Please wait a moment before sending another message."
|
| 94 |
+
resp.raise_for_status()
|
| 95 |
+
return resp.json()["reply"]
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
async def _get_reply(message: str, chat_history: List[Dict[str, str]]) -> str:
|
| 99 |
+
"""Fetch the full reply from the LLM (API or direct pipeline)."""
|
| 100 |
+
if API_URL:
|
| 101 |
+
return await _chat_via_api(message, chat_history)
|
| 102 |
+
result = await pipeline.chat(message=message, history=chat_history)
|
| 103 |
+
return result["reply"]
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
async def chat_fn(message: str, chat_history: List[Dict[str, str]], session_id: str = ""):
|
| 107 |
+
"""Streaming generator: yields word-by-word with human-like timing & typos."""
|
| 108 |
+
if not message or not message.strip():
|
| 109 |
+
yield gr.update(interactive=True), chat_history
|
| 110 |
+
return
|
| 111 |
+
|
| 112 |
+
original_history = chat_history.copy()
|
| 113 |
+
chat_history = chat_history + [
|
| 114 |
+
{"role": "user", "content": message},
|
| 115 |
+
{"role": "assistant", "content": ""}
|
| 116 |
+
]
|
| 117 |
+
yield gr.update(value="", interactive=False), chat_history
|
| 118 |
+
|
| 119 |
+
try:
|
| 120 |
+
reply = await _get_reply(message, original_history)
|
| 121 |
+
except httpx.HTTPStatusError as e:
|
| 122 |
+
logger.error(f"HTTP Error: {e.response.status_code} - {e.response.text}")
|
| 123 |
+
reply = "⚠️ Rate limit reached. Please slow down a bit!" if e.response.status_code == 429 \
|
| 124 |
+
else "⚠️ I encountered an error. Please try again in a few seconds."
|
| 125 |
+
except Exception as e:
|
| 126 |
+
logger.error(f"Unexpected error: {str(e)}\n{traceback.format_exc()}")
|
| 127 |
+
reply = f"⚠️ Oops! Something went wrong."
|
| 128 |
+
|
| 129 |
+
# ── Humanistic letter-by-letter typing — HTML-safe ────────────────────
|
| 130 |
+
displayed = "" # what is shown in the chat bubble
|
| 131 |
+
buffer = "" # invisible accumulator for mid-HTML-tag characters
|
| 132 |
+
in_tag = False # True while we are inside a < … > sequence
|
| 133 |
+
|
| 134 |
+
for char in reply:
|
| 135 |
+
if char == '<':
|
| 136 |
+
in_tag = True
|
| 137 |
+
buffer += char
|
| 138 |
+
continue
|
| 139 |
+
|
| 140 |
+
if in_tag:
|
| 141 |
+
buffer += char
|
| 142 |
+
if char == '>':
|
| 143 |
+
# Tag is now complete — flush it to the display all at once
|
| 144 |
+
in_tag = False
|
| 145 |
+
displayed += buffer
|
| 146 |
+
buffer = ""
|
| 147 |
+
chat_history[-1] = {"role": "assistant", "content": displayed}
|
| 148 |
+
yield gr.update(value="", interactive=False), chat_history
|
| 149 |
+
# Small pause after a <br> to mimic line-break pacing
|
| 150 |
+
if displayed.endswith('<br>') or displayed.endswith('<br/>'):
|
| 151 |
+
await asyncio.sleep(random.uniform(0.1, 0.2))
|
| 152 |
+
continue
|
| 153 |
+
|
| 154 |
+
# ── Normal character (not inside a tag) ───────────────────────────
|
| 155 |
+
# Decide typo chance only for plain alphabetic words
|
| 156 |
+
do_typo = (
|
| 157 |
+
char.isalpha()
|
| 158 |
+
and len(displayed) > 8 # skip the very beginning
|
| 159 |
+
and random.random() < TYPO_CHANCE
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
if do_typo:
|
| 163 |
+
wrong_char = _nearby_char(char)
|
| 164 |
+
displayed += wrong_char
|
| 165 |
+
chat_history[-1] = {"role": "assistant", "content": displayed}
|
| 166 |
+
yield gr.update(value="", interactive=False), chat_history
|
| 167 |
+
await asyncio.sleep(random.uniform(0.15, 0.3))
|
| 168 |
+
|
| 169 |
+
# Backspace the wrong character
|
| 170 |
+
displayed = displayed[:-1]
|
| 171 |
+
chat_history[-1] = {"role": "assistant", "content": displayed}
|
| 172 |
+
yield gr.update(value="", interactive=False), chat_history
|
| 173 |
+
await asyncio.sleep(random.uniform(0.1, 0.2))
|
| 174 |
+
|
| 175 |
+
# Type the correct character
|
| 176 |
+
displayed += char
|
| 177 |
+
chat_history[-1] = {"role": "assistant", "content": displayed}
|
| 178 |
+
yield gr.update(value="", interactive=False), chat_history
|
| 179 |
+
|
| 180 |
+
# Pacing: punctuation pauses, space micro-pause, normal char speed
|
| 181 |
+
if char in '.!?':
|
| 182 |
+
await asyncio.sleep(random.uniform(0.35, 0.7))
|
| 183 |
+
elif char in ',:;':
|
| 184 |
+
await asyncio.sleep(random.uniform(0.15, 0.3))
|
| 185 |
+
elif char == ' ':
|
| 186 |
+
await asyncio.sleep(random.uniform(0.06, 0.13))
|
| 187 |
+
else:
|
| 188 |
+
await asyncio.sleep(random.uniform(0.02, 0.06))
|
| 189 |
+
|
| 190 |
+
# ── Fire-and-forget: persist to session store (never blocks the UI) ───
|
| 191 |
+
# We only save successful responses, not error messages
|
| 192 |
+
if session_id and not displayed.startswith("⚠️"):
|
| 193 |
+
asyncio.create_task(_ss.save_message(session_id, message, displayed))
|
| 194 |
+
|
| 195 |
+
# Re-enable the input box at the end
|
| 196 |
+
yield gr.update(interactive=True), chat_history
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
custom_css = """
|
| 200 |
+
/* Aggressively force height: auto and min-height: 40px on the chatbot and flex containers */
|
| 201 |
+
.gradio-container, .flex, .block, #chatbot-window {
|
| 202 |
+
height: auto !important;
|
| 203 |
+
min-height: 40px !important;
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
/* Ensure the chatbot doesn't overflow the viewport if it gets too full */
|
| 207 |
+
#chatbot-window {
|
| 208 |
+
max-height: 70vh !important;
|
| 209 |
+
border: none !important;
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
/* Aggressively hide the footer, including HF injected footers if inside the container */
|
| 213 |
+
footer, .footer, footer * {
|
| 214 |
+
display: none !important;
|
| 215 |
+
visibility: hidden !important;
|
| 216 |
+
height: 0 !important;
|
| 217 |
+
margin: 0 !important;
|
| 218 |
+
padding: 0 !important;
|
| 219 |
+
}
|
| 220 |
+
"""
|
| 221 |
+
|
| 222 |
+
# JS: runs immediately on page load — generates/loads session_id from localStorage
|
| 223 |
+
# and restores previous chat history. Completely non-blocking.
|
| 224 |
+
_SESSION_JS = """
|
| 225 |
+
async () => {
|
| 226 |
+
// ── Session ID: generate once, persist forever in localStorage ──
|
| 227 |
+
let sid = localStorage.getItem('martech_session_id');
|
| 228 |
+
if (!sid) {
|
| 229 |
+
sid = 'sess-' + Date.now() + '-' + Math.random().toString(36).slice(2, 9);
|
| 230 |
+
localStorage.setItem('martech_session_id', sid);
|
| 231 |
+
}
|
| 232 |
+
// Write the session_id into the hidden Gradio textbox
|
| 233 |
+
const el = document.getElementById('session-id-box')?.querySelector('textarea');
|
| 234 |
+
if (el) { el.value = sid; el.dispatchEvent(new Event('input', {bubbles:true})); }
|
| 235 |
+
return sid;
|
| 236 |
+
}
|
| 237 |
+
"""
|
| 238 |
+
|
| 239 |
+
with gr.Blocks(title="Martechsol Assistant", theme=gr.themes.Soft(), css=custom_css) as demo:
|
| 240 |
+
gr.Markdown("# Martechsol Assistant")
|
| 241 |
+
gr.Markdown("Welcome! How can I help you today?")
|
| 242 |
+
|
| 243 |
+
chatbot = gr.Chatbot(
|
| 244 |
+
show_label=False,
|
| 245 |
+
elem_id="chatbot-window",
|
| 246 |
+
render_markdown=True,
|
| 247 |
+
)
|
| 248 |
+
with gr.Row():
|
| 249 |
+
msg = gr.Textbox(
|
| 250 |
+
placeholder="Type your question here...",
|
| 251 |
+
show_label=False,
|
| 252 |
+
scale=9
|
| 253 |
+
)
|
| 254 |
+
send = gr.Button("Send", variant="primary", scale=1)
|
| 255 |
+
|
| 256 |
+
clear = gr.Button("Clear Chat History")
|
| 257 |
+
|
| 258 |
+
# Hidden textbox holds the session_id — invisible to the user
|
| 259 |
+
session_id = gr.Textbox(
|
| 260 |
+
value="",
|
| 261 |
+
visible=False,
|
| 262 |
+
elem_id="session-id-box",
|
| 263 |
+
label="session_id",
|
| 264 |
+
)
|
| 265 |
+
|
| 266 |
+
# On load: run JS to set session_id from localStorage (non-blocking)
|
| 267 |
+
demo.load(fn=None, inputs=None, outputs=session_id, js=_SESSION_JS)
|
| 268 |
+
|
| 269 |
+
# Wire up the events — streaming generator for live typing effect (UNCHANGED)
|
| 270 |
+
send.click(chat_fn, inputs=[msg, chatbot, session_id], outputs=[msg, chatbot])
|
| 271 |
+
msg.submit(chat_fn, inputs=[msg, chatbot, session_id], outputs=[msg, chatbot])
|
| 272 |
+
clear.click(lambda: [], None, chatbot)
|
| 273 |
+
|
| 274 |
+
if __name__ == "__main__":
|
| 275 |
+
demo.launch(server_name="0.0.0.0", server_port=7860, show_api=False)
|
backup_2026-05-04/docker-compose.yml
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: "3.9"
|
| 2 |
+
services:
|
| 3 |
+
rag-api:
|
| 4 |
+
build: .
|
| 5 |
+
container_name: rag-api
|
| 6 |
+
ports:
|
| 7 |
+
- "8000:8000"
|
| 8 |
+
env_file:
|
| 9 |
+
- .env
|
| 10 |
+
volumes:
|
| 11 |
+
- ./docs:/app/docs
|
| 12 |
+
- ./data/index:/app/data/index
|
| 13 |
+
- ./data/sessions:/app/data/sessions
|
| 14 |
+
restart: unless-stopped
|
backup_2026-05-04/docs/MartechSol_Employee_HandBook (7) (1).pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a48b14cc28d7892ae03a5dfe06a244832e2807ce178ca1f1acdbc58d6af4e8ef
|
| 3 |
+
size 3844502
|
backup_2026-05-04/docs/MartechSol_Employee_Handbook_Extracted.txt
ADDED
|
@@ -0,0 +1,1091 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Employee Handbook
|
| 2 |
+
Last Modified: September 1st, 2024
|
| 3 |
+
|
| 4 |
+
Contents
|
| 5 |
+
Introduction ....................................................................................................... 4
|
| 6 |
+
Workplace Orientation ....................................................................................... 4
|
| 7 |
+
Employment Status .......................................................................................... 5
|
| 8 |
+
Employment At Will .......................................................................................... 5
|
| 9 |
+
Employee Responsibilities ........................................................................................ 5
|
| 10 |
+
Workweek and Workday .................................................................................... 5
|
| 11 |
+
Time Clocks and Time Cards ............................................................................... 5
|
| 12 |
+
General Attendance Requirements ....................................................................... 6
|
| 13 |
+
Work from Home .............................................................................................. 6
|
| 14 |
+
Problem Resolution ........................................................................................... 7
|
| 15 |
+
Personal Appearance ......................................................................................... 7
|
| 16 |
+
Health Safety .................................................................................................. 7
|
| 17 |
+
Employee Relationships ..................................................................................... 8
|
| 18 |
+
Policy Against Harassment ................................................................................. 8
|
| 19 |
+
Activity Permission ........................................................................................... 9
|
| 20 |
+
Confidentiality ................................................................................................ 9
|
| 21 |
+
Intangible Property ......................................................................................... 10
|
| 22 |
+
Restraint of Trade, Non-Compete, and Cooling Off Period ....................................... 11
|
| 23 |
+
Solicitation ................................................................................................... 11
|
| 24 |
+
Cell Phone Usage ........................................................................................... 12
|
| 25 |
+
Computer, E-Mail and Internet Policy ................................................................. 12
|
| 26 |
+
Tobacco Use .................................................................................................. 13
|
| 27 |
+
Grievance Procedure ....................................................................................... 13
|
| 28 |
+
Copyright Violation ......................................................................................... 13
|
| 29 |
+
Data Theft .................................................................................................... 13
|
| 30 |
+
Separation from the Company .......................................................................... 14
|
| 31 |
+
Exit Interviews .............................................................................................. 14
|
| 32 |
+
Final Settlement ............................................................................................. 14
|
| 33 |
+
Return of Property .......................................................................................... 15
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
Acceptability ................................................................................................. 15
|
| 37 |
+
Work Rules ................................................................................................... 15
|
| 38 |
+
Level 1 Actions .............................................................................................. 16
|
| 39 |
+
Level 2 Actions .............................................................................................. 16
|
| 40 |
+
Level 3 Actions .............................................................................................. 17
|
| 41 |
+
Disciplinary Procedures ..................................................................................... 18
|
| 42 |
+
Employee Benefits ................................................................................................. 18
|
| 43 |
+
Referral Program ............................................................................................ 18
|
| 44 |
+
Game Room .................................................................................................. 18
|
| 45 |
+
Holidays and Holiday Pay ................................................................................. 18
|
| 46 |
+
Pay Period and Paychecks ................................................................................ 19
|
| 47 |
+
Leaves ......................................................................................................... 19
|
| 48 |
+
Sick Leaves ................................................................................................... 19
|
| 49 |
+
Casual Leaves ............................................................................................... 20
|
| 50 |
+
Annual Leaves ............................................................................................... 21
|
| 51 |
+
Hajj Leave .................................................................................................... 22
|
| 52 |
+
Maternity Leave ............................................................................................. 23
|
| 53 |
+
Paternity Leave .............................................................................................. 23
|
| 54 |
+
Bereavement Leave ........................................................................................ 24
|
| 55 |
+
Unauthorized Leaves ....................................................................................... 24
|
| 56 |
+
Unapproved Absence Without Pay ..................................................................... 25
|
| 57 |
+
Leave Encashment ......................................................................................... 25
|
| 58 |
+
Other Benefits ..................................................................................................... 25
|
| 59 |
+
Loans & Advance Salary .................................................................................. 25
|
| 60 |
+
Maternity (Pregnancy) Benefit .......................................................................... 26
|
| 61 |
+
Company Maintained Vehicle ............................................................................ 27
|
| 62 |
+
Healthcare Insurance – For Self & Spouse ............................................................ 27
|
| 63 |
+
EOBI: .......................................................................................................... 30
|
| 64 |
+
Provident Fund: ............................................................................................. 30
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
Introduction
|
| 68 |
+
We are providing you this employee handbook to introduce you to our policies. The handbook will
|
| 69 |
+
describe some of the more important employee benefits and obligations.
|
| 70 |
+
Our business is constantly changing, and therefore MartechSol reserves the right to change all of our
|
| 71 |
+
policies, including those covered in this handbook, at any time. You will be informed of any changes
|
| 72 |
+
in policy through posting on portal or through other appropriate means. If you are uncertain about
|
| 73 |
+
any policy or procedure, please check with your Supervising Authority.
|
| 74 |
+
Workplace Orientation
|
| 75 |
+
Orientati
|
| 76 |
+
on Training and Initial Evaluation Period
|
| 77 |
+
All employees will receive orientation training at the beginning of employment with MartechSol. This
|
| 78 |
+
period of orientation will include familiarization with Logicose’s work policies, disciplinary policies, job
|
| 79 |
+
duties, and reporting responsibilities.
|
| 80 |
+
Upon completion of orientation, all employees will undergo evaluation during the first ninety (90)
|
| 81 |
+
calendar days of employment. If performance is unsatisfactory for any reason during this period of time,
|
| 82 |
+
the employee may be immediately dismissed from employment. At the end of this 90-day period, the
|
| 83 |
+
employee’s Supervising Authority will determine if the employee’s performance warrants continued
|
| 84 |
+
employment.
|
| 85 |
+
If the employee’s performance is not up to the Supervising Authority’s expectations in terms of quality of
|
| 86 |
+
work, output, behavior, or any other performance indicator, the employee will not be confirmed for
|
| 87 |
+
further employment at the company after this 90-day period. It is also up to the Supervising Authority to
|
| 88 |
+
end the probation period at any point before 90 days and either confirm or terminate the employee.
|
| 89 |
+
The Supervising Authority is required to make a decision regarding the confirmation of an employee after
|
| 90 |
+
90 days. If the Supervising Authority requires more time to evaluate an employee further, (s)he has the
|
| 91 |
+
right extend the probation period.
|
| 92 |
+
Confirmation
|
| 93 |
+
If performance exceeds expectations, an employee may receive a salary increment upon confirmation.
|
| 94 |
+
The date of confirmation depends on date of joining and performance. Revised salaries for employees
|
| 95 |
+
that are confirmed before the 15th of a given month will be effective from the 1st of that same month. On
|
| 96 |
+
the other hand, revised salaries will be effective from the 1st of the following month for employees that
|
| 97 |
+
are confirmed after the 15th of a given month. For example, if someone is confirmed on the 10th of April,
|
| 98 |
+
the revised salary will be effective from the 1st April, however if they are confirmed on the 16th of April,
|
| 99 |
+
the revised salary will be effective from the 1st of May.
|
| 100 |
+
Casual and sick leaves will also be assigned on a pro-rata basis based on this calculation.
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
Employment Status
|
| 104 |
+
A “regular full-time employee” is any employee who is employed to work on a year-round basis, and who
|
| 105 |
+
is regularly scheduled to work their full shift hours.
|
| 106 |
+
Employment At Will
|
| 107 |
+
All employees are employed on an "at-will" basis. This means that employees are not employed for any
|
| 108 |
+
specific term or period of time, and that the employer may terminate the employment relationship at
|
| 109 |
+
any time for any reason. Similarly, employees may resign and terminate employment with MartechSol
|
| 110 |
+
at any time and for any reason with prior notice of 30-90 days depending on their role within the
|
| 111 |
+
organization.
|
| 112 |
+
N
|
| 113 |
+
o employee has any authority to change the nature of this relationship through any statements,
|
| 114 |
+
representations, or conduct. The at-will relationship may be changed only through written and signed
|
| 115 |
+
authorization of the Supervising Authority.
|
| 116 |
+
Employee Responsibilities
|
| 117 |
+
Workweek and Workday
|
| 118 |
+
The nor
|
| 119 |
+
mal workweek shall be considered full shift hours, six days per week, from 9:00 a.m. to 5:30 p.m.
|
| 120 |
+
The first Saturday of every month is off and the remaining three Saturday’s in each month are six and a
|
| 121 |
+
half hours (half-day). One hour is allocated for lunch/dinner in all shifts.
|
| 122 |
+
Morning Shift: 1:30 pm - 2:30 pm
|
| 123 |
+
Evening Shift: 9:30 pm - 10:30 pm
|
| 124 |
+
Smoking Breaks: There are a maximum of 3 breaks allowed in a workday (other than lunch/dinner
|
| 125 |
+
break). These breaks are not to be longer than 5 minutes.
|
| 126 |
+
If any other break is required, the employee is to contact his or her Supervising Authority for approval.
|
| 127 |
+
Time Clocks and Time Cards
|
| 128 |
+
All empl
|
| 129 |
+
oyees shall use the portal to record their starting time and ending time. For wage payment
|
| 130 |
+
purposes, time worked will be figured to the nearest tenth of an hour.
|
| 131 |
+
Sharing login information (username and password) is strictly prohibited. Each person is to use their own
|
| 132 |
+
login information to time in at work. If any discrepancy is observed in the time in or time out process,
|
| 133 |
+
harsh disciplinary action will be taken against the person or persons involved.
|
| 134 |
+
In case an employee forgets to time in or is unable to time in, they are to submit a trouble ticket to the
|
| 135 |
+
Human Resources department. It is the employee’s responsibility to maintain their time sheet accurately.
|
| 136 |
+
Monthly salary will be calculated according to the time sheet.
|
| 137 |
+
Employees who want to change their work shift should contact their Supervising Authority for approval.
|
| 138 |
+
Necessary paperwork and legitimate reason are required for the shift change process. The decision to
|
| 139 |
+
change the shift of an employee is at the discretion of the Supervising Authority.
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
General Attendance Requirements
|
| 143 |
+
Regular and pro
|
| 144 |
+
mpt attendance of employees is essential to our business. Employees who cannot meet
|
| 145 |
+
our attendance requirements will be subject to counseling and disciplinary action. Excessive absenteeism
|
| 146 |
+
or tardiness will result in termination of employment. The Supervisor has the authority to mark a half
|
| 147 |
+
day, day off, or full day at his/her discretion.
|
| 148 |
+
Late Coming: Late coming is permitted till 20 minutes from your shift start time. If you arrive
|
| 149 |
+
after 20 minutes of your shift start time, then it will be considered as a late arrival. 4 late arrivals
|
| 150 |
+
in a month will be considered as a half day off. In addition, an employee is to inform his/her
|
| 151 |
+
Supervising Authority if they will be more than 30 minutes late to work.
|
| 152 |
+
Early Out: If you need to leave before your shift ends, verbal approval is mandatory from your
|
| 153 |
+
Supervising Authority. There will be no deductions for early out.
|
| 154 |
+
Half Day: Half day will be marked if you arrive 2 hours after your shift start time. If you complete
|
| 155 |
+
less than six and a half hours on any given day, then it will also be considered as a half day off.
|
| 156 |
+
Full Day: Employees who spend less than 2 hours at work on any workday will be marked absent
|
| 157 |
+
that day.
|
| 158 |
+
Employees who are late to work on any given workday are still responsible to complete all the
|
| 159 |
+
work assigned to them. If they are unable to, they should contact their Supervising Authority
|
| 160 |
+
immediately. If an employee arrives at work after 1 hour of their shift start time, by default,
|
| 161 |
+
the 8.5 hours shift has to be completed. If an employee is more than 1 hour late and wants
|
| 162 |
+
to
|
| 163 |
+
leave early (without completing the total (8.5 hr) shift time), he/she has to contact their
|
| 164 |
+
Supervising Authority for approval.
|
| 165 |
+
For
|
| 166 |
+
employees who are unable to complete at least 50% of their work during a workday, the
|
| 167 |
+
Supervising Authority reserves the right to decide the status of the workday (full day, off day,
|
| 168 |
+
half day) is at his or her discretion.
|
| 169 |
+
Work from Home
|
| 170 |
+
In case o
|
| 171 |
+
f emergency (bad weather, temporary disability, unrest in city, etc) when employees cannot
|
| 172 |
+
reach work, they will be requested to work from home. In case an employee has personal valid reason(s)
|
| 173 |
+
as to why they cannot reach work, they can work from home requesting approval from their Supervising
|
| 174 |
+
Authority. The approval of this request is at the Supervising Authority’s discretion.
|
| 175 |
+
If less than 30% of the work has been completed, the day will be counted as a full day off. If 30-70% of
|
| 176 |
+
the work has been completed, it will be counted as a half day. If more than 70% of the work has been
|
| 177 |
+
completed, it will be counted as a full day.
|
| 178 |
+
Work can be submitted no later than 3 hours after the end of the employee’s regular shift timing. If the
|
| 179 |
+
work is not submitted within the given time, it will not be marked as work provided for the same day. If
|
| 180 |
+
an employee faces problems submitting their work within the given duration and wants to submit their
|
| 181 |
+
work later, approval is required from their Supervising Authority.
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
Problem Resolution
|
| 185 |
+
If an emplo
|
| 186 |
+
yee faces difficulty or problems related to anything within the workplace, it is their
|
| 187 |
+
responsibility to submit a trouble ticket through the Portal. There is no need to contact the Supervising
|
| 188 |
+
Authority unless their ticket has not been addressed for more than 24 hours.
|
| 189 |
+
You are requested to use this trouble ticket system for your computer, admin and HR related
|
| 190 |
+
issues. How to use this system:
|
| 191 |
+
1. If you have any computer, admin, or HR related issue, go to the tickets link in the portal and
|
| 192 |
+
select ‘Submit a ticket’.
|
| 193 |
+
2. Select the concerned department for your problem e.g. if your mouse, keyboard, CPU or LCD is
|
| 194 |
+
not working properly, please select Networking. If there is any issue with your attendance or
|
| 195 |
+
payroll, select Human Resources. If there is any issue with fan, AC, kitchen item etc. please select
|
| 196 |
+
Admin department.
|
| 197 |
+
3. Once your trouble ticket is submitted, you may view the status of your ticket from ‘View open
|
| 198 |
+
tickets’ link.
|
| 199 |
+
As soon as your ticket is submitted, the Company will make every possible effort to resolve your issue on
|
| 200 |
+
an urgent basis.
|
| 201 |
+
There is also a messaging feature in this trouble ticket system. If there is any update related to your
|
| 202 |
+
ticket, you will receive a message within your open ticket. Similarly, if you want to send any message
|
| 203 |
+
related to your problem, you can also do that from ‘View open tickets’ section.
|
| 204 |
+
Once your issue is resolved, your ticket will be closed. You may view your closed tickets from ‘View your
|
| 205 |
+
closed tickets’ section.
|
| 206 |
+
This system will help resolve your issues quickly and will allow you to view status of your open tickets.
|
| 207 |
+
Personal Appearance
|
| 208 |
+
A company
|
| 209 |
+
is judged by its employees, as well as by its products and services. All employees are
|
| 210 |
+
expected to maintain good standards of personal neatness, including hair, clothes, and shoes.
|
| 211 |
+
Casual to business-style dress is appropriate. Employees should be neatly groomed and clothes should be
|
| 212 |
+
clean and in good condition. Leisure clothes such as cut-offs or halter tops are not acceptable attire for
|
| 213 |
+
the business office.
|
| 214 |
+
Health Safety
|
| 215 |
+
If an employee has had a contagious illness or serious health issue which may affect the health of other
|
| 216 |
+
employees (e.g. chicken pox, flu), a doctor’s written permission to return to work will be necessary.
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
Employee Relationships
|
| 220 |
+
MartechSol employs a number of people and, as a result, you will be working with people whose
|
| 221 |
+
personalities may differ from yours. It is your responsibility to get along with all of your co-workers. A
|
| 222 |
+
spirit of cooperation shown by all of us will help to accomplish our purpose of providing new
|
| 223 |
+
customers with the best possible service.
|
| 224 |
+
This does not mean that you need to become a personal friend of all your co-workers, but it does mean
|
| 225 |
+
that you should treat your co-workers with respect. Regardless of your personal opinions, your fellow
|
| 226 |
+
employee should be treated exactly as you would like to have that person treat you.
|
| 227 |
+
Senior employees should remember that they were once new employees and therefore should do
|
| 228 |
+
everything possible to aid new employees in acclimating themselves to their new jobs.
|
| 229 |
+
Policy
|
| 230 |
+
Against Harassment
|
| 231 |
+
MartechSol is committed to maintaining a work environment that is free of discrimination and
|
| 232 |
+
harassment. In keeping with this commitment, we will not tolerate harassment of our employees by
|
| 233 |
+
anyone, including any Supervising Authority, co-worker, vendor, client, or customer.
|
| 234 |
+
Harassment consists of unwelcome conduct, whether verbal, physical, or visual, that is based upon a
|
| 235 |
+
person’s protected status, such as sex, color, race, ancestry, religion, national origin, age, physical
|
| 236 |
+
handicap, medical condition, disability, marital status, veteran status, citizenship status, or other
|
| 237 |
+
protected group status.
|
| 238 |
+
MartechSol will not tolerate harassing conduct that affects tangible job benefits, that interferes
|
| 239 |
+
unreasonably with an individual’s work performance, or that creates an intimidating, hostile, or
|
| 240 |
+
offensive working environment.
|
| 241 |
+
Sexual harassment deserves special mention. Unwelcome sexual advances, requests for sexual favors,
|
| 242 |
+
and other physical, verbal, or visual conduct based on sex constitute sexual harassment when (1)
|
| 243 |
+
submission to the conduct is made a term or condition of employment, (2) submission to or rejection of
|
| 244 |
+
the conduct is used as the basis for an employment decision, or (3) the conduct has the purpose or effect
|
| 245 |
+
of unreasonably interfering with an individual’s work performance or creating an intimidating, hostile, or
|
| 246 |
+
offensive working environment. Sexual harassment may include explicit sexual proposition, sexual
|
| 247 |
+
innuendo, suggestive comments, sexually oriented “kidding” or “teasing,” “practical jokes,” jokes about
|
| 248 |
+
gender-specific traits, foul or obscene language or gestures, displays of foul or obscene printed or visual
|
| 249 |
+
material, and physical contact, such as patting, pinching, or brushing against another’s body.
|
| 250 |
+
All employees are responsible for helping to assure we avoid harassment. If you feel that you
|
| 251 |
+
have experienced or witnessed harassment, you should immediately notify your Supervising
|
| 252 |
+
Authority. MartechSol forbids retaliation against anyone who has reported harassment.
|
| 253 |
+
It is our policy to investigate all such complaints thoroughly and promptly. To the fullest extent
|
| 254 |
+
practicable, MartechSol will keep complaints and the terms of their resolution confidential. If an
|
| 255 |
+
investigation confirms that harassment has occurred, MartechSol will take appropriate corrective
|
| 256 |
+
action, including discipline up to and including immediate dismissal from employment.
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
Activity Permission
|
| 260 |
+
MartechSol is concerned with outside activities of employees only where such activities might involve a
|
| 261 |
+
conflict of interest with Logicose’s best interests. A conflict of interest exists if an employee has a
|
| 262 |
+
private financial interest or other relationship outside MartechSol that is detrimental to the best
|
| 263 |
+
interests of MartechSol.
|
| 264 |
+
You shall not without prior written consent of the company, engage directly or indirectly, in any trade,
|
| 265 |
+
business or occupation or any other income generation activity.
|
| 266 |
+
If an employee desires to attend an academic institution during his/her employment, (s)he must receive
|
| 267 |
+
permission from the company by submitting relevant documentation. It is mandatory to fill the Activity
|
| 268 |
+
Permission form for approval.
|
| 269 |
+
An employee should discuss any possible conflict of interest with the HR Department as full disclosure
|
| 270 |
+
and open knowledge are essential. For the employee’s own protection, written approval should be
|
| 271 |
+
obtained in all cases where a possible conflict of interest is involved.
|
| 272 |
+
Other possible conflict of interest situations may include any type of “moonlighting work” or a second
|
| 273 |
+
job, which interferes with the employee’s work for MartechSol, any type of external business
|
| 274 |
+
relationship with MartechSol, any of its competitors or any of its customers, and any type of public
|
| 275 |
+
activity which may affect adversely on MartechSol.
|
| 276 |
+
Confidentiality
|
| 277 |
+
MartechSol is a service provider that creates, completes, and delivers work for clients that do not want
|
| 278 |
+
our involvement disclosed in their projects. The ownership of the work we create for our clients is
|
| 279 |
+
transferred over to them and they hold the copyright to the work delivered. Disclosing such work may
|
| 280 |
+
result in Copyright Infringement, so it is in your, and the company’s best interest, to not share
|
| 281 |
+
involvement in such work.
|
| 282 |
+
Moreover, employees are prohibited from maintaining records of confidential information or intellectual
|
| 283 |
+
property on personal e-mail accounts, computers, hard drives, smart phones, cloud, or online.
|
| 284 |
+
Both during employment with MartechSol and after separation, employees are not permitted to
|
| 285 |
+
disclose to any third party or share any confidential information that is made available to you.
|
| 286 |
+
Confidential information includes, but is not limited to:
|
| 287 |
+
Web copy, eBooks, Press Releases, Reports, Infographics, Articles, Blogs, and/or any copy that was
|
| 288 |
+
created or revised for clients while employed at MartechSol is the property of either the clients or
|
| 289 |
+
MartechSol. Disclosing involvement in such projects to those outside the company, mentioning it
|
| 290 |
+
on your CV or resume, and/or making public your involvement in such projects is prohibited.
|
| 291 |
+
Search Engine Optimization strategy, campaigns, processes, proposals, accounts for posting, list
|
| 292 |
+
of clients, contact information of clients, content created or revised for client is confidential
|
| 293 |
+
information. Mentioning your involvement and/or disclosing such information to those outside
|
| 294 |
+
the company or on your CV or resume is considered breach of policy.
|
| 295 |
+
Client Information, leads, pricing strategy, proposals, account information, portal information,
|
| 296 |
+
samples, and promotional campaigns are the intellectual property of MartechSol. They may
|
| 297 |
+
neither be disclosed nor shared with those outside the organization.
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
Sensitive company information, such as employee contact details, software code, promotional
|
| 301 |
+
campaigns, client list, project(s) details, performance evaluation system, and in-house software.
|
| 302 |
+
Any and all such information is either Logicose’s or its Client’s intellectual property.
|
| 303 |
+
Immediately upon termination of employment, or at any other time upon the company’s request, all
|
| 304 |
+
employees will return to the Company:
|
| 305 |
+
All memoranda, notes and data, and computer software and hardware, records or other
|
| 306 |
+
data/documents compiled by the employee or made available to the employee during the
|
| 307 |
+
employee’s employment with the company concerning the business of the company, its
|
| 308 |
+
affiliates, or their customers.
|
| 309 |
+
All other Confidential Information that is disclosed to you during employment.
|
| 310 |
+
All physical and intellectual property of the company or its affiliates, including without limitation,
|
| 311 |
+
all the content, information, plans, files, records, documents, lists, equipment, supplies,
|
| 312 |
+
promotional materials, keys, vehicles, and similar items and all copies thereof or extracts there
|
| 313 |
+
from.
|
| 314 |
+
Disclosure of such information while employed or after separation from MartechSol is unethical, and
|
| 315 |
+
such actions will be considered intellectual property theft.
|
| 316 |
+
Furthermore, you cannot claim ownership of any project or developed content/material in any manner.
|
| 317 |
+
For example, giving a reference on your CV to a specific website for which you developed content while
|
| 318 |
+
working at the Company is prohibited. Any project or client details relating to your work carried out at
|
| 319 |
+
this company cannot be mentioned on your CV.
|
| 320 |
+
Intangible Property
|
| 321 |
+
During your course of employment with MartechSol, you will come across information that is privy to
|
| 322 |
+
those outside the organization. Such information is the intellectual (intangible) property of the company.
|
| 323 |
+
You may not at any time, during or after employment with MartechSol, have or claim any right, title, or
|
| 324 |
+
interest in any trade name, trademark, patent, copyright, work for hire, or other similar rights belonging
|
| 325 |
+
to or used by the company and shall not have or claim any right, title, or interest in any material or
|
| 326 |
+
matter of any sort prepared for or used in connection with the business or promotion of the company,
|
| 327 |
+
whatever your involvement with such matters may have been, and whether procured, produced,
|
| 328 |
+
prepared, or published in whole or in part by you.
|
| 329 |
+
MartechSol, or its clients, have and will retain the sole and exclusive rights in any and all such trade
|
| 330 |
+
names, trademarks, patents, copyrights, material, and matter unless transferred over to a third party.
|
| 331 |
+
Any and all documents, software, or copy that was created for MartechSol, its clients, or its affiliate
|
| 332 |
+
brand names, is the intellectual property of the company or its clients. You may not claim ownership,
|
| 333 |
+
maintain personal records of, or disclose your involvement in the creation or modification of such
|
| 334 |
+
information, software, or projects.
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
Restraint of Trade, Non-Compete, and Cooling Off Period
|
| 338 |
+
Any current or separated employee is prohibited from incorporating a new company or starting a new
|
| 339 |
+
business in the same industry before the time of at least 24 months.
|
| 340 |
+
Employee shall not, on his or her own behalf or behalf of others, hold any ownership interest in any
|
| 341 |
+
business engaged in the following industries:
|
| 342 |
+
Copywriting
|
| 343 |
+
Online Marketing
|
| 344 |
+
SEO
|
| 345 |
+
SMO
|
| 346 |
+
Ghostwriting
|
| 347 |
+
Industries that MartechSol, or any of its brand names, are currently or in the process of operating in
|
| 348 |
+
Employee agrees and covenants that the use of sensitive and confidential information that is made available,
|
| 349 |
+
in certain circumstances, may cause irreparable damage to MartechSol and its reputation. Therefore,
|
| 350 |
+
employee shall not until the expiration of two years after the termination of the employment,
|
| 351 |
+
through any corporations or associates in any business, form a business that is directly competitive with
|
| 352 |
+
Company for a period of two years.
|
| 353 |
+
Since the region or territory in which MartechSol operates is based online, the jurisdiction of the
|
| 354 |
+
restraint of trade is not limited to a specific city, state, or country. Forming an organization that is based
|
| 355 |
+
in any city MartechSol operates in, and/or creation of an online business is a breach of policy.
|
| 356 |
+
Failure to comply with activity permission, restraint of trade, confidentiality, intangible property, non-
|
| 357 |
+
disclosure, and non-compete will result in immediate termination. The Company also reserves the right
|
| 358 |
+
to take appropriate legal action upon breach of policy both during employment and after separation.
|
| 359 |
+
Solicitation
|
| 360 |
+
Both during employment with the company and for a period of two (2) years following the termination/
|
| 361 |
+
resignation of employment with the company at any time and for any reason, employee’s will not, directly
|
| 362 |
+
or indirectly, on the employee’s own behalf or on behalf of any other person or entity, hire or solicit to
|
| 363 |
+
hire for employment or consulting other provision of services, any person who is actively employed or
|
| 364 |
+
engaged (or in the preceding six months was actively employed or engaged) by MartechSol.
|
| 365 |
+
This includes, but is not limited to, inducting or attempting to induce, or influencing or attempting to
|
| 366 |
+
influence, any person employed or engaged by the company to terminate his or her relationship with the
|
| 367 |
+
company. MartechSol reserves the right to take appropriate legal action against solicitation of employees.
|
| 368 |
+
Furthermore, both during the employee’s employment and after separation from the company at any
|
| 369 |
+
time and for any reason, the employee will not directly or indirectly, on the employee’s own behalf of
|
| 370 |
+
any other person or entity, solicit the business of any Customer or any other entity with which
|
| 371 |
+
MartechSol has worked with or discussed the possibility of any work, at the time of employee’s
|
| 372 |
+
termination, to provide services to such entity (a “Contractor”). Client information is confidential, and
|
| 373 |
+
MartechSol reserves the right to take legal action upon disclosure of privy information.
|
| 374 |
+
|
| 375 |
+
|
| 376 |
+
Cell Phone Usage
|
| 377 |
+
Emp
|
| 378 |
+
loyees shall not use cell phones for personal purposes while engaged in work activities. If the
|
| 379 |
+
employee needs to use the cell phone for important personal reasons, the employee shall excuse
|
| 380 |
+
themselves from the premises. Keep your cell phone on silent/vibrate. Use it responsibly, and make sure
|
| 381 |
+
that you do not disturb others while answering your calls.
|
| 382 |
+
Computer, E-Mail and Internet Policy
|
| 383 |
+
MartechSol maintains certain policy requirements for use of its “electronic communication systems.”
|
| 384 |
+
“Electronic communication systems” include computers, file servers, electronic information storage
|
| 385 |
+
systems, internal and external electronic mail systems, voice mail systems, local area networks and
|
| 386 |
+
any other system or technology used now or in the future by MartechSol to store or transmit data of
|
| 387 |
+
any kind electronically.
|
| 388 |
+
Our
|
| 389 |
+
policy requires the following:
|
| 390 |
+
1. Employees shall use the electronic communications systems for business purposes.
|
| 391 |
+
2. Logicose’s electronic communications systems and all information transmitted or received by, or
|
| 392 |
+
stored or contained in, these systems are the property of MartechSol. No employee shall have
|
| 393 |
+
any property rights in or expectation of privacy for any information, even if of a personal nature,
|
| 394 |
+
communicated or received through Logicose’s electronic communications systems. MartechSol
|
| 395 |
+
may review, delete and copy any such information without first obtaining the consent of the
|
| 396 |
+
employee who created, sent or received the information. No employee, including system
|
| 397 |
+
administrators and Supervising Authority, shall use any electronic communications systems for
|
| 398 |
+
purposes of satisfying idle curiosity about the affairs of others. There must be a business purpose
|
| 399 |
+
for obtaining access to the files or communications of others.
|
| 400 |
+
3. No employee shall use the electronic communications systems in a manner that constitutes a
|
| 401 |
+
violation of provincial or federal law or that is offensive to others.
|
| 402 |
+
4. Due to the threat to system stability posed by computer viruses and system incompatibility, no
|
| 403 |
+
employee shall install or download software from the Internet or elsewhere on his or her
|
| 404 |
+
computer without the supervision of the office administrator.
|
| 405 |
+
If you have any questions about the policy or its administration, please contact your Supervising
|
| 406 |
+
Authority. Violations of the policy may result in disciplinary action up to and including termination.
|
| 407 |
+
USBs cannot be used on any office computer. If necessary, please contact the System Administrator.
|
| 408 |
+
The completion of assigned work is the employee’s responsibility. If an employee has any issues
|
| 409 |
+
regarding their PC or internet, they are to raise a trouble ticket immediately.
|
| 410 |
+
The use of the following is prohibited:
|
| 411 |
+
Social media websites (e.g. Facebook)
|
| 412 |
+
Chatting websites (e.g. Meebo)
|
| 413 |
+
Chatting applications (e.g. MSN Messenger, Skype)
|
| 414 |
+
Online gaming websites (e.g. miniclips.com)
|
| 415 |
+
E-mail sites (e.g. Hotmail)
|
| 416 |
+
|
| 417 |
+
|
| 418 |
+
Political Websites
|
| 419 |
+
Pornography
|
| 420 |
+
Tobacco Use
|
| 421 |
+
MartechSol maintains a smoke-free workplace for all of its employees, which is why smoking is
|
| 422 |
+
prohibited in the workplace. Employees who must smoke are encouraged to do so away from Company
|
| 423 |
+
property and during work and/or meal breaks only.
|
| 424 |
+
Grievance
|
| 425 |
+
Procedure
|
| 426 |
+
MartechSol has a grievance procedure to make sure that problems and complaints an employee has
|
| 427 |
+
concerning his or her treatment or working conditions is called to our attention. This does not
|
| 428 |
+
include computer-related issues or similar technical issues. For such problems, the trouble ticket
|
| 429 |
+
system is provided. Employees with a problem should take the following steps:
|
| 430 |
+
1. Explai
|
| 431 |
+
n the problem to your immediate Supervising Authority. Allow 48 hours for an answer.
|
| 432 |
+
2. If you are still not satisfied, put your grievance in writing and submit it to MartechSol President via
|
| 433 |
+
the Feedback Form on the Portal. If possible, a decision will be given within three working days.
|
| 434 |
+
Copyright Violation
|
| 435 |
+
Any work performed within the premises of MartechSol for its clients, employees, or the Company itself
|
| 436 |
+
is under the sole ownership of MartechSol or those that the company transfers the rights to. Employees
|
| 437 |
+
cannot claim ownership of this work or use it outside company premises for other purposes, this
|
| 438 |
+
includes creating personal records on computers or e-mail accounts that aren’t the property of
|
| 439 |
+
MartechSol. Employees cannot use this work as reference to anyone else either, unless they get written
|
| 440 |
+
consent from MartechSol. Listed below are some examples of copyright violation:
|
| 441 |
+
Showing, sharing, using, creating a personal record of, or claiming ownership or involvement in the
|
| 442 |
+
creation of:
|
| 443 |
+
Co
|
| 444 |
+
py written
|
| 445 |
+
Software developed
|
| 446 |
+
Client contact information
|
| 447 |
+
Payments received at MartechSol
|
| 448 |
+
In
|
| 449 |
+
tellectual property
|
| 450 |
+
Data Theft
|
| 451 |
+
Any data relating to MartechSol, its clients, or employees is under the sole ownership of MartechSol.
|
| 452 |
+
Employees cannot use or transmit this data, as it is not under their ownership. Employees cannot use
|
| 453 |
+
this data as reference either.
|
| 454 |
+
Archiving such work on external hard drives, online, cloud, and/or on computers that are not property
|
| 455 |
+
of MartechSol is a breach of policy.
|
| 456 |
+
|
| 457 |
+
|
| 458 |
+
Separation from the Company
|
| 459 |
+
Prio
|
| 460 |
+
r to Confirmation:
|
| 461 |
+
o Minimum one day notice period, or as communicated in the offer letter, is required
|
| 462 |
+
during probationary period on behalf of the employee.
|
| 463 |
+
o If employee performance/behavior is not up to Company expectations, then the
|
| 464 |
+
Supervising Authority has the right to end the probation period and terminate the
|
| 465 |
+
employee.
|
| 466 |
+
After Confirmation:
|
| 467 |
+
o The employment relationship may be terminated either by you or by the company with a
|
| 468 |
+
prior written notice of one month or salary in lieu thereof, unless mentioned below. It is,
|
| 469 |
+
however, understood that no notice is required in case your services are terminated on
|
| 470 |
+
grounds of misconduct, willful neglect of duty, breach of trust or any other dereliction of
|
| 471 |
+
duty or misdemeanor prejudicial to the interest of the company. It is further understood
|
| 472 |
+
that any financial indebtedness of yours, standing to the detriment of the company’s
|
| 473 |
+
prestige will amount to a very serious misdemeanor towards the company
|
| 474 |
+
o A minimum notice period of at least 30 days is to served unless listed below otherwise:
|
| 475 |
+
Managerial Roles
|
| 476 |
+
Vice Team Lead 30 days
|
| 477 |
+
Team Lead 60 days
|
| 478 |
+
Group Team Lead 90 days
|
| 479 |
+
Exit Interviews
|
| 480 |
+
All employees separating from MartechSol must complete an exit interview with their immediate
|
| 481 |
+
supervisor. An exit interview gives the Human Resource Department the opportunity to obtain feedback
|
| 482 |
+
from employees as they transition out of their positions.
|
| 483 |
+
Final Settlement
|
| 484 |
+
All employees must undergo a final settlement before they separate from the organization. All
|
| 485 |
+
adjustments will be made according to below mentioned procedures:
|
| 486 |
+
Bef
|
| 487 |
+
ore Salary is Processed:
|
| 488 |
+
If any employee resigns before the salary is processed of any given month, their adjustments
|
| 489 |
+
(loans, advances, pro-rata leaves, etc.) will be made in the same month and the salary will be
|
| 490 |
+
deposited along with the rest of the employees. The remaining amount of the following month’s
|
| 491 |
+
salary will be processed as per the payroll process. No adjustments will be made in the following
|
| 492 |
+
salary except for early exit. For example, someone resigns on the 20th of April and his last working
|
| 493 |
+
day is the 20th of May, the adjustments will be made in the month of April’s salary. The salary for
|
| 494 |
+
the remaining days of the notice period to be served in May will be processed with May’s payroll
|
| 495 |
+
with Early Exit deduction of 11 days (21st May-31st May).
|
| 496 |
+
After/At the time Salary is Processed:
|
| 497 |
+
If any employee resigns once the salary is processed of any given month, their adjustments (loans,
|
| 498 |
+
advances, pro-rata leaves, etc.) will be made in the following month and the salary will be
|
| 499 |
+
deposited along with the rest of the employees. For example, someone resigns on 25th of April and
|
| 500 |
+
|
| 501 |
+
|
| 502 |
+
his last working day is on the 25th of May, the adjustments will be made in the month of May’s
|
| 503 |
+
salary.
|
| 504 |
+
Return of Property
|
| 505 |
+
Employees who separate from MartechSol service must return all issued property such as identification
|
| 506 |
+
cards, health insurance card, company maintained vehicle, and intellectual property of the organization.
|
| 507 |
+
Acceptability
|
| 508 |
+
Any changes in Policies, Rules, and Code of Conduct will first be shared through the MartechSol Portal.
|
| 509 |
+
A reasonable period of time will be allotted to discuss suggestions and amendments in changes before
|
| 510 |
+
its implementation.
|
| 511 |
+
Timing In to work after date of implementation implies that you understand and will abide by
|
| 512 |
+
organization policy.
|
| 513 |
+
Work
|
| 514 |
+
Rules
|
| 515 |
+
Any work performed within the premises of MartechSol is under the Company’s ownership and/or its
|
| 516 |
+
clients. Employees cannot claim ownership of any work done in or for MartechSol, neither can they use
|
| 517 |
+
it in the future.
|
| 518 |
+
All work performed at MartechSol has to be done in a legal and ethical manner so there is never
|
| 519 |
+
any copyright violation.
|
| 520 |
+
Employees shall perform their assignments within the specifics of the position description. Employees
|
| 521 |
+
who consistently fail to conform to the specifics of their position description or exhibit inappropriate
|
| 522 |
+
behavior or poor performance shall be required to meet with their supervisor.
|
| 523 |
+
This meeting will attempt to identify the problems, find ways to improve the situation and suggest
|
| 524 |
+
adequate solutions, concluding with a recommended course of action and an appropriate time frame
|
| 525 |
+
in which the employee will be expected to improve to the satisfaction of MartechSol. Details of the
|
| 526 |
+
meeting will be documented, signed by all parties as a correct representation of points discussed and
|
| 527 |
+
placed in the employee’s personal file.
|
| 528 |
+
Disciplinary action and corrective measures are taken at the discretion of the Management at
|
| 529 |
+
MartechSol. The possible steps for disciplinary action are:
|
| 530 |
+
1. Co
|
| 531 |
+
unseling/Verbal Warning
|
| 532 |
+
2. One or more formal written warnings through email
|
| 533 |
+
3. Issuance of Notice
|
| 534 |
+
4. Suspension
|
| 535 |
+
5. Dismissal
|
| 536 |
+
The choice of options depends on the seriousness of the behavior. Exceptions or deviations from the
|
| 537 |
+
progressive discipline sequence may occur whenever the Supervising Authority, in conjunction with the
|
| 538 |
+
President/Director, deems that circumstances warrant.
|
| 539 |
+
For level 1 offense, discussions between the employee and his or her supervisor will occur to allow the
|
| 540 |
+
employee to correct the situation. When a warning notice is issued, it becomes a part of an employee's
|
| 541 |
+
|
| 542 |
+
|
| 543 |
+
record and is considered when evaluating an employee for promotion, transfer, training or additional
|
| 544 |
+
discipline. Various offenses have been divided into different levels according to severity.
|
| 545 |
+
Three warning notices within twelve (12) months’ time, regardless of the type of level offense, will result
|
| 546 |
+
in discharge.
|
| 547 |
+
Level 1 Actions
|
| 548 |
+
These actions are taken for behaviors such as:
|
| 549 |
+
Unautho
|
| 550 |
+
rized or excessive absence, tardiness or early timing out
|
| 551 |
+
Unauthorized time away from work station
|
| 552 |
+
Slowing or interfering with the work of other employees
|
| 553 |
+
Failure to notify supervisor of incompletion of assigned work before leaving
|
| 554 |
+
Obscene, abusive, harassing, or disruptive language or behavior
|
| 555 |
+
Failure to perform assigned job responsibilities
|
| 556 |
+
Failure to follow prescribed work procedures
|
| 557 |
+
Failure to notify supervisor of absences
|
| 558 |
+
Neglect of organization property
|
| 559 |
+
Excessive personal use of cell telephone and/or email
|
| 560 |
+
Work shift not completed
|
| 561 |
+
Procedure For Dealing With Level 1 Behavior: These procedures are at the discretion of the Supervising
|
| 562 |
+
Authority.
|
| 563 |
+
Counseling/Verbal warning
|
| 564 |
+
Formal written warning in the form of email
|
| 565 |
+
Issuance of notice
|
| 566 |
+
Suspension
|
| 567 |
+
Level 2 Actions
|
| 568 |
+
These are more serious and must be dealt with firmly and immediately. Typical behaviors in this level
|
| 569 |
+
include:
|
| 570 |
+
Reoccurrin
|
| 571 |
+
g tardiness without reasonable explanation
|
| 572 |
+
Absences without approved leave
|
| 573 |
+
Refusal to comply with instructions of a supervisor
|
| 574 |
+
Conduct endangering the safety of the employee, co-workers or members
|
| 575 |
+
Working when ability is impaired by the use of alcohol, and/or illegal drugs
|
| 576 |
+
Unscheduled leaving from the workplace without informing supervisor
|
| 577 |
+
Habitual sleeping during work hours
|
| 578 |
+
Unauthorized use of organization materials and supplies
|
| 579 |
+
Fighting or threatening violence in the workplace
|
| 580 |
+
Unauthorized possession of weapons on organization property
|
| 581 |
+
|
| 582 |
+
|
| 583 |
+
Knowingly timing in another employee
|
| 584 |
+
Sharing portal login info
|
| 585 |
+
Inappropriate usage of internet, or any usage of internet unrelated to work during work hours
|
| 586 |
+
Harassing or bullying coworkers
|
| 587 |
+
Procedure for dealing with Level 2 behavior: These procedures are at the discretion of the Supervising
|
| 588 |
+
Authority.
|
| 589 |
+
Issuance of Notice
|
| 590 |
+
Suspension or probation
|
| 591 |
+
Termination
|
| 592 |
+
Level 3 Actions
|
| 593 |
+
These are b
|
| 594 |
+
ehaviors that are serious enough to justify either a suspension or, in extreme situations,
|
| 595 |
+
termination of employment without following the preceding disciplinary steps. Behaviors for which
|
| 596 |
+
immediate termination can be justified include, but are not limited to, the following:
|
| 597 |
+
Sexual harassment
|
| 598 |
+
Involvement in any income-generating activity without the consent of supervising authority
|
| 599 |
+
Insubordination, or the refusal to comply with the specific instructions of a supervisor in the
|
| 600 |
+
context of an assigned job duty
|
| 601 |
+
Falsification of personnel records, time records, or any other organization documents and records
|
| 602 |
+
Fighting during work time or on work premises
|
| 603 |
+
Use of, or possession of, alcohol or illegal drugs during work time or on work property
|
| 604 |
+
Damaging, defacing, or misusing organization property or the property of coworkers
|
| 605 |
+
Theft, misappropriation, embezzlement, unauthorized possession or removal of organization
|
| 606 |
+
property or the property of employees or customers
|
| 607 |
+
Immoral or indecent conduct which occurs on organization property
|
| 608 |
+
Possession of explosives, firearms, or other dangerous weapons on work premises
|
| 609 |
+
Failure to report an absence for a three-day period without a satisfactory explanation
|
| 610 |
+
Unauthorized release of work-related data or information
|
| 611 |
+
Continued unsatisfactory job performance
|
| 612 |
+
Violation of the organization's conflict of interest/ethical standards
|
| 613 |
+
Data theft: Stealing any digital or other type of data (e.g. email, company records, client records)
|
| 614 |
+
which belongs to MartechSol.
|
| 615 |
+
Copyright violation: Showing ownership of specific work performed at MartechSol. Discussion
|
| 616 |
+
of clients, websites, revenue generated, payments, or any work-related details outside the
|
| 617 |
+
Company’s premises.
|
| 618 |
+
Ot
|
| 619 |
+
her behaviors that, in the opinion of the Supervising Authority, seriously threaten the well-
|
| 620 |
+
being of co-workers.
|
| 621 |
+
In case of any ambiguity, please contact your supervising authority.
|
| 622 |
+
|
| 623 |
+
|
| 624 |
+
Procedure for dealing with Level 3 behavior: These procedures are at the discretion of the Supervising
|
| 625 |
+
Authority.
|
| 626 |
+
Suspension
|
| 627 |
+
Termination
|
| 628 |
+
Disciplinary Procedures
|
| 629 |
+
MartechSol may use progressive discipline as a means of enforcing minor violations of the work rules.
|
| 630 |
+
Progressive discipline may include steps such as counseling, written warning, and suspension.
|
| 631 |
+
Depending on the nature of the misconduct and the position involved, steps in the process may be
|
| 632 |
+
omitted or employment may be immediately terminated. The decision whether to apply progressive
|
| 633 |
+
discipline in any particular situation will rest within the sole discretion of management.
|
| 634 |
+
Examples of misconduct which will usually result in immediate discharge include theft, fighting or
|
| 635 |
+
physical assault, insubordination, dishonesty, falsification of time cards, production records, or other
|
| 636 |
+
Company documents, possession of alcohol or illegal drugs in the workplace, and disloyalty or breach of
|
| 637 |
+
confidentiality. This list is not intended to limit our right to discharge in other situations when we believe
|
| 638 |
+
termination of employment is appropriate.
|
| 639 |
+
Employee Benefits
|
| 640 |
+
Referral Program
|
| 641 |
+
Any employ
|
| 642 |
+
ee can benefit from the referral program by referring qualified friends or acquaintances.
|
| 643 |
+
The employee who has referred an individual is eligible for a PKR. 5,000 bonus once the new
|
| 644 |
+
candidate joins the organization.
|
| 645 |
+
Game Room
|
| 646 |
+
In order to promote a relaxed environment and provide employees with more forms of
|
| 647 |
+
entertainment, MartechSol has set up a game room in Room 301.
|
| 648 |
+
This game ro
|
| 649 |
+
om is accessible during lunchtime, so the morning shift has access to it from 1:30-2:30 pm
|
| 650 |
+
and the evening/night shift has access from 9:00-10:00pm. The game room can also be used after work.
|
| 651 |
+
Females are to use the game room on Monday, Wednesday, and Friday during lunchtime. Males should
|
| 652 |
+
use the game room on Tuesday, Thursday, and Saturday during lunch hour.
|
| 653 |
+
Holidays and Holiday Pay
|
| 654 |
+
MartechSol observes all Federal holidays as paid holidays. Due to our nature of work, employees
|
| 655 |
+
may be asked to work on specific holidays, according to their availability. They will be awarded
|
| 656 |
+
compensatory leave allowance or compensatory leave (employee’s choice), which they can utilize at
|
| 657 |
+
their convenience.
|
| 658 |
+
To qualify for holiday pay, an employee must have worked his or her regularly scheduled shift on the
|
| 659 |
+
day preceding the holiday and must work his or her regularly scheduled shift on the day following the
|
| 660 |
+
holiday.
|
| 661 |
+
|
| 662 |
+
|
| 663 |
+
Pay Period and Paychecks
|
| 664 |
+
For pay c
|
| 665 |
+
alculation purposes, the workweek (or “pay period”) will commence 12:01 a.m. on Sunday
|
| 666 |
+
morning and end at 12:00 p.m. on Saturday evening. Employees will be paid monthly on (payday), for the
|
| 667 |
+
preceding month. If payday falls on a holiday, then paychecks will be provided the day before the
|
| 668 |
+
holiday.
|
| 669 |
+
Employees are expected to immediately report any mistakes in their paychecks. Underpayments will be
|
| 670 |
+
corrected as soon as possible. If it is not practical to issue an employee a new paycheck to correct any
|
| 671 |
+
overpayment, the overpayment will be deducted from the employee's next paycheck.
|
| 672 |
+
Salary deduction for days off will be on “per-day” basis. {(Gross Salary x 12) / 365}.
|
| 673 |
+
Payroll for June and December
|
| 674 |
+
The salary for June and December will be processed and transferred during the first week of July and
|
| 675 |
+
January respectively. This is to ensure that the transition from the current to the revised salary is smooth
|
| 676 |
+
and all adjustments for the preceding month are processed correctly.
|
| 677 |
+
Leaves
|
| 678 |
+
Prior to Confirmation:
|
| 679 |
+
No leaves are allowed prior to your confirmation.
|
| 680 |
+
In case you skip a day, you will be marked absent, and your salary will be deducted for the day.
|
| 681 |
+
Prior approval from your supervising authority is required for situations where you would be
|
| 682 |
+
unable to come into work.
|
| 683 |
+
If you are absent from work as a result of sickness or any other unforeseeable personal matter,
|
| 684 |
+
you are required to notify your supervisory authority or the HR department by telephone or SMS
|
| 685 |
+
within two hours of your scheduled shift starting time.
|
| 686 |
+
After Confirmation:
|
| 687 |
+
Sick Leaves
|
| 688 |
+
Definition:
|
| 689 |
+
Sick Leave is defined as the period of time an employee is absent from work with full pay as a
|
| 690 |
+
result of an illness or injury.
|
| 691 |
+
Policy:
|
| 692 |
+
All full time confirmed employees are entitled for a maximum of 8 days of sick leave per annum.
|
| 693 |
+
Sick leaves cannot be carried forward to the next year and neither can they be encashed.
|
| 694 |
+
Each year on the 1st of January, your leave account will be credited with the sick leave
|
| 695 |
+
entitlement.
|
| 696 |
+
If anyone is confirmed in MartechSol during the year, then his/her sick leave entitlement will be
|
| 697 |
+
on a pro-rata basis.
|
| 698 |
+
In case of separation from the company during the year, sick leave availed will be adjusted on
|
| 699 |
+
|
| 700 |
+
|
| 701 |
+
pro- rata basis from your final salary payment.
|
| 702 |
+
If an employee takes sick leave from work starting any day of the week, any weekend or holiday
|
| 703 |
+
that falls in between will be counted as a full day sick leave.
|
| 704 |
+
Sick leave availed in excess of entitlement will be treated as casual or annual leave (if available
|
| 705 |
+
and subject to approval from reporting authority); incase of insufficient casual/annual leave
|
| 706 |
+
balance, excess sick leave will be treated as unpaid leave.
|
| 707 |
+
During notice period, no sick leave will be entertained even if the leave shows credit balance.
|
| 708 |
+
Process and Documentation:
|
| 709 |
+
Planned appointments, surgery, or any medical instance which can be deemed as anticipated is
|
| 710 |
+
subject to prior approval from your supervising authority.
|
| 711 |
+
If you are absent from work as a result of sickness or injury, you are required to notify your
|
| 712 |
+
supervisory authority or HR department by telephone or SMS within two hours of your
|
| 713 |
+
scheduled shift starting time (or before if possible). Failure to comply with the process may result
|
| 714 |
+
in absent without leave.
|
| 715 |
+
You must apply for sick leave immediately after returning from your sick leave by submitting
|
| 716 |
+
your leave application to the HR department after approval from your supervising authority.
|
| 717 |
+
The submission of a medical certificate or relevant documentation when applying for a sick leave
|
| 718 |
+
is mandatory, the leave will be considered absent without pay if proper documentation is not
|
| 719 |
+
submitted.
|
| 720 |
+
Sick leave can only be availed when sick and cannot be claimed for other purposes. Falsifying
|
| 721 |
+
information or using sick leave for a purpose other than illness or injury will make you liable for
|
| 722 |
+
disciplinary action.
|
| 723 |
+
Casual Leaves
|
| 724 |
+
Definition:
|
| 725 |
+
Casual leaves may be availed by employees in cases of urgent or unforeseen contingencies.
|
| 726 |
+
These cases include, but are not limited to, situations which may arise on account of unforeseen
|
| 727 |
+
circumstances e.g. birth/illness/death in the immediate family or of a close relative.
|
| 728 |
+
Any personal reason that prevents you from coming into work can be used for your casual leave.
|
| 729 |
+
Policy:
|
| 730 |
+
All full time confirmed employees are entitled for a maximum of 10 days casual leave per annum.
|
| 731 |
+
Casual leaves cannot be carried forward to the next year.
|
| 732 |
+
Casual leaves that have not been exhausted over the calendar year will be subject to encashment
|
| 733 |
+
with the paycheck of the following month.
|
| 734 |
+
Each year on the 1st of January, your leave account will be credited with the casual leave
|
| 735 |
+
entitlement if eligible.
|
| 736 |
+
If anyone is confirmed in MartechSol during the year, then his/her casual leave entitlement will
|
| 737 |
+
be on a pro-rata basis.
|
| 738 |
+
In case of separation from the company during the year, casual leave availed will be adjusted on
|
| 739 |
+
|
| 740 |
+
|
| 741 |
+
pro-rata basis from your final salary payment.
|
| 742 |
+
If an employee takes casual leave from work starting any day of the week, any weekend or
|
| 743 |
+
holiday that falls in between will be counted as a full day casual leave.
|
| 744 |
+
Casual leave cannot be taken for 3 consecutive days at any given time. If leave availed is for or in
|
| 745 |
+
excess of 3 days, the entire period will be treated as unpaid leave. However, if circumstances
|
| 746 |
+
warrant, the department head may approve the first 3 days to be treated as casual leave, and
|
| 747 |
+
only the additional days to be treated as annual leaves (if available).
|
| 748 |
+
Employees can take up to 2 consecutive casual leaves. If the number of casual leaves is 3 or
|
| 749 |
+
more, the whole duration will be treated as unpaid automatically unless approved by the line
|
| 750 |
+
manager.
|
| 751 |
+
In case of any urgent requirement, your reporting authority may ask you to report to work during
|
| 752 |
+
your casual leave.
|
| 753 |
+
Combining casual leave with a weekly holiday or a public holiday shall be inadmissible and that
|
| 754 |
+
additional leave shall be adjusted as leave without pay. For example, if a public holiday falls on
|
| 755 |
+
Friday and any employee takes leave on Saturday, all three days will be counted as casual leave.
|
| 756 |
+
During notice period, no casual leave will be allowed even if the leave shows credit balance.
|
| 757 |
+
Documentation and Process:
|
| 758 |
+
If you are absent from work as a result of any urgent personal responsibilities or any
|
| 759 |
+
unforeseeable circumstances, you are required to notify your supervisory authority or HR
|
| 760 |
+
department by telephone or SMS within two hours of your scheduled shift starting time (or
|
| 761 |
+
before if possible). Failure to comply with the process may result in absent without leave.
|
| 762 |
+
You must apply for casual leave immediately after returning from your casual leave by submitting
|
| 763 |
+
your leave application to the HR department after approval from your supervising authority.
|
| 764 |
+
In cases of planned or known circumstances, an employee must apply for casual leave to the
|
| 765 |
+
immediate supervisor for approval in advance.
|
| 766 |
+
Annual Leaves
|
| 767 |
+
Definition:
|
| 768 |
+
The purpose of annual leave is to provide employees with a reasonable period of rest and
|
| 769 |
+
a significant break from the workplace in order to maintain a healthy work life balance.
|
| 770 |
+
Policy:
|
| 771 |
+
All Full-time confirmed employee(s) shall be entitled to 14 days paid annual leave after
|
| 772 |
+
completion of one year of service.
|
| 773 |
+
Annuals leaves are not subject to encashment, except for TL roles or greater and for the Sales &
|
| 774 |
+
Support Department.
|
| 775 |
+
Up to 7 annual leaves can be carried forward to the next year, if approved by supervising
|
| 776 |
+
authority, for the following reasons:
|
| 777 |
+
Marriage
|
| 778 |
+
Obligation outside the country (Hajj, Residence Visa Process, etc.)
|
| 779 |
+
Or for any planned reason that warrants extended leave.
|
| 780 |
+
|
| 781 |
+
|
| 782 |
+
Carried forward annual leaves can’t be carried forward for more than a year.
|
| 783 |
+
If any employee completes one year of service at MartechSol during the calendar year, then his/
|
| 784 |
+
her annual leave entitlement will be on a pro-rata basis for that year.
|
| 785 |
+
In case of separation from the company during the year, annual leave availed will be adjusted on
|
| 786 |
+
pro-rata basis from your final salary payment.
|
| 787 |
+
Annual leave may be availed at one time during the year or may be divided and availed at various
|
| 788 |
+
times during the year, subject to approval.
|
| 789 |
+
Annual leaves will be granted in keeping with the operational exigencies of the organization, and
|
| 790 |
+
will be scheduled by the supervisor to meet the requirements of individual employees as far as
|
| 791 |
+
possible.
|
| 792 |
+
Management reserves the right to refuse a leave request, allow a partial request, revoke
|
| 793 |
+
approval if already granted, and/or to recall an employee before expiry of the leave period.
|
| 794 |
+
If you are hospitalized during your annual leave and produce a valid medical certificate stating
|
| 795 |
+
the fact, then these days will be adjusted as sick leave (if available).
|
| 796 |
+
If an employee takes annual leave from work starting any day of the week, any weekend or
|
| 797 |
+
holiday that falls in between will be counted as an annual leave.
|
| 798 |
+
During notice period, no annual leave will be allowed even if the leave shows credit balance.
|
| 799 |
+
Documentation and Process:
|
| 800 |
+
A completed leave application form is to be submitted for approval in advance to the HR
|
| 801 |
+
department prior to the anticipated start of the leave accordingly:
|
| 802 |
+
Number of leaves Approval time required
|
| 803 |
+
1-3 days Before 2 days
|
| 804 |
+
4-7 days Before 1 week
|
| 805 |
+
More than 7 days Before 1 month
|
| 806 |
+
An employee who wishes to obtain annual leaves shall apply in writing to HR in advance before
|
| 807 |
+
the commencement of leaves.
|
| 808 |
+
If the leave request is refused or postponed on account of company exigencies, the fact of such
|
| 809 |
+
refusal or postponement and reasons thereto shall be conveyed to the employee concerned.
|
| 810 |
+
If the requested leave is granted the employee will proceed on his leave on the approved date.
|
| 811 |
+
If an employee remains absent beyond the approved period of leave, he shall be liable for
|
| 812 |
+
disciplinary action unless the employee has justifiable reasons for absenteeism.
|
| 813 |
+
Annual leaves may only be availed after prior approval has been granted, however in case of an
|
| 814 |
+
emergency, annual leaves may be entertained subject to your supervising authority’s discretion.
|
| 815 |
+
Hajj Leave
|
| 816 |
+
Definition:
|
| 817 |
+
Employees who wish to make their pilgrimage to Mecca during Dhu'l Hijja may do so during
|
| 818 |
+
their tenure at MartechSol. These leaves are separate from the employee's annual leaves or any
|
| 819 |
+
other leave which he/she is entitled to.
|
| 820 |
+
|
| 821 |
+
|
| 822 |
+
Policy:
|
| 823 |
+
All full time permanent employees with 4 years of service are entitled for paid Hajj Leave for 10
|
| 824 |
+
days.
|
| 825 |
+
Any leaves beyond 10 days needs to come from your annual or casual leaves.
|
| 826 |
+
Hajj leave will be granted only once during the whole employment period.
|
| 827 |
+
|
| 828 |
+
Documentation and Process:
|
| 829 |
+
Your leave application must be submitted three months in advance for the necessary approval.
|
| 830 |
+
Copy of your Hajj Passport/Visa needs to be submitted to HR.
|
| 831 |
+
Maternity Leave
|
| 832 |
+
Definition:
|
| 833 |
+
A period of approved absence for female employees that wish to continue employment
|
| 834 |
+
at MartechSol after delivery, granted for the purpose of giving birth and taking care of
|
| 835 |
+
newborn children.
|
| 836 |
+
Policy:
|
| 837 |
+
All full-time female employees shall be entitled to 90 days paid maternity leave, taken no later
|
| 838 |
+
than two weeks before the expected delivery date and is only applicable after completion of one
|
| 839 |
+
year of service.
|
| 840 |
+
This entitlement shall be available only twice during the entire period of service.
|
| 841 |
+
Premature birth shall be considered as maternity leave. In case of still birth, female employee
|
| 842 |
+
shall be entitled to 30 days paid maternity leave.
|
| 843 |
+
Documentation and Process:
|
| 844 |
+
Maternity Leave form needs to be submitted and approved five months before planned
|
| 845 |
+
maternity leave.
|
| 846 |
+
|
| 847 |
+
Paternity Leave
|
| 848 |
+
Definition:
|
| 849 |
+
A period of approved absence for male employees granted after or shortly before the birth of his
|
| 850 |
+
child.
|
| 851 |
+
Policy:
|
| 852 |
+
All full-time male employees are entitled to 3 days paid paternity leave.
|
| 853 |
+
This entitlement shall be available only twice during the entire period of service.
|
| 854 |
+
|
| 855 |
+
|
| 856 |
+
|
| 857 |
+
Male employees may combine other leaves with paternity leave subject to approval from
|
| 858 |
+
supervising authority.
|
| 859 |
+
Premature birth shall be considered as paternity leave. In case of still birth, male employee shall
|
| 860 |
+
be entitled to up to 3 days paid paternity leave.
|
| 861 |
+
Documentation and Process:
|
| 862 |
+
Application for paternity leave should be submitted to HR department at least two months in
|
| 863 |
+
advance supported by the gynecologist’s certification.
|
| 864 |
+
Bereavement Leave
|
| 865 |
+
Definition:
|
| 866 |
+
Bereavement leaves are paid leaves for employees that cover absences related to the death of
|
| 867 |
+
immediate family members.
|
| 868 |
+
Policy:
|
| 869 |
+
All full-time employee(s) shall be entitled to 3 days paid bereavement leave.
|
| 870 |
+
Bereavement leave will normally be granted unless there are unusual business needs or staffing
|
| 871 |
+
requirements. An employee may, with his or her supervisor’s approval, use any available leaves
|
| 872 |
+
for additional time off as necessary.
|
| 873 |
+
Documentation and Process:
|
| 874 |
+
You must apply for bereavement leave immediately after returning from your leave by
|
| 875 |
+
submitting your leave application to the HR department after approval from your supervising
|
| 876 |
+
authority.
|
| 877 |
+
Unauthorized Leaves
|
| 878 |
+
Any
|
| 879 |
+
absence without prior approval from your reporting authority will be considered as an
|
| 880 |
+
unauthorized leave.
|
| 881 |
+
All unauthorized and/or unreported absences shall be considered Absences without Leave,
|
| 882 |
+
(AWOL). Each unauthorized leave will result in a disciplinary notice and will be added in your file.
|
| 883 |
+
3 unauthorized leaves in a quarter will result in a severe disciplinary action.
|
| 884 |
+
In case you were not able to inform your reporting authority due to some unavoidable reasons,
|
| 885 |
+
you may discuss it with your reporting authority later. Based on discretion, he/she may authorize
|
| 886 |
+
your leave.
|
| 887 |
+
For employees serving their probationary period, failure to communicate with the respective
|
| 888 |
+
authority for more than 3 consecutive days of leave will result in termination.
|
| 889 |
+
For permanent employees, failure to communicate with the respective authority for more than 7
|
| 890 |
+
consecutive days of leave will result in termination.
|
| 891 |
+
|
| 892 |
+
|
| 893 |
+
Unapproved Absence Without Pay
|
| 894 |
+
Definition:
|
| 895 |
+
Any leave, in excess of the 10 Casual leaves that you are entitled to, is an Absence without Pay.
|
| 896 |
+
These leaves, when taken without prior approval from your supervising authority are considered
|
| 897 |
+
Unapproved Absence without Pay.
|
| 898 |
+
Policy:
|
| 899 |
+
|
| 900 |
+
Unapproved Absence without Pay will result in salary deduction(s) per day absent.
|
| 901 |
+
Taking more than two consecutive Absences without Pay prior to the weekend will result in salary
|
| 902 |
+
deduction for the weekend. For example, if an employee is absent on Thursday, Friday, and
|
| 903 |
+
Saturday then Sunday will also be marked absent. In other words, employees must report to work
|
| 904 |
+
on the last working day of the week to avoid further deductions.
|
| 905 |
+
Leave Encashment
|
| 906 |
+
Each employee is entitled to a number of Sick, Casual, Annual, and other leaves. However, only Casual
|
| 907 |
+
leaves are subject to encashment for specific departments.
|
| 908 |
+
Listed below is breakdown of Department eligibility of leave encashment:
|
| 909 |
+
Department Casual Annual
|
| 910 |
+
Copywriting Yes No
|
| 911 |
+
SEO Yes No
|
| 912 |
+
Sales Yes Yes
|
| 913 |
+
Software Yes No
|
| 914 |
+
Networking Yes No
|
| 915 |
+
Human Resources Yes No
|
| 916 |
+
Admin Yes No
|
| 917 |
+
Other Benefits
|
| 918 |
+
Loans & Advance Salary
|
| 919 |
+
Definition:
|
| 920 |
+
The purpose of loans and advance salary is to meet an emergency need that can’t be
|
| 921 |
+
accommodated through other financial arrangements. Only genuine emergency needs such as
|
| 922 |
+
extraordinary medical costs not covered by insurance, or any other extreme financial or medical
|
| 923 |
+
emergency will be considered. The decision to approve or reject a loan/advance salary request will
|
| 924 |
+
be taken by a 3-person committee which will comprise of line manager/head of department, a
|
| 925 |
+
representative from HR, and a representative from finance.
|
| 926 |
+
Policy:
|
| 927 |
+
Decisions regarding loan requests will be relayed within 3 to 7 working days. For extremely urgent
|
| 928 |
+
matters, the committee will take a decision within 27 hours of request sent.
|
| 929 |
+
The decision of the committee will be final and non-negotiable.
|
| 930 |
+
A loans/advance salary can only be taken once in 6 months.
|
| 931 |
+
|
| 932 |
+
|
| 933 |
+
If an employee has an ongoing loan/advance salary, he/she will not be allowed to avail another
|
| 934 |
+
loan/advance salary.
|
| 935 |
+
Repayment period is up to 6 months.
|
| 936 |
+
Loan amount can’t exceed 3 months’ salary.
|
| 937 |
+
Advance salary amount shall not exceed from your monthly salary.
|
| 938 |
+
Documentation & Process:
|
| 939 |
+
If possible, the loan/advance salary form should be submitted at least a week (7 days) before it is
|
| 940 |
+
required.
|
| 941 |
+
The form can be acquired from HR and must be completed including all the signatures required.
|
| 942 |
+
The necessary documentation & supporting documents should be provided along with the filled
|
| 943 |
+
form.
|
| 944 |
+
Maternity (Pregnancy) Benefit
|
| 945 |
+
Definition:
|
| 946 |
+
This benefit provides financial assistance to female and male employees to cover the cost of childbirth and
|
| 947 |
+
other delivery related expenses.
|
| 948 |
+
Policy:
|
| 949 |
+
Male and female employees that have completed at least one year of service can avail maternity
|
| 950 |
+
benefit of PKR 50,000.
|
| 951 |
+
Maternity benefit can be utilized twice during tenure at MartechSol.
|
| 952 |
+
This benefit also covers stillbirth.
|
| 953 |
+
Documentation & Process:
|
| 954 |
+
Maternity Benefit Form and relevant documentation must be provided at least 60 days before
|
| 955 |
+
expected delivery.
|
| 956 |
+
Employees will be awarded this benefit a week before date of expected delivery.
|
| 957 |
+
In case of premature delivery, employees can either contact Supervising Authority or Admin
|
| 958 |
+
Department to arrange the benefit urgently or they may pay out of pocket at the time and avail the
|
| 959 |
+
benefit when they rejoin the office.
|
| 960 |
+
|
| 961 |
+
|
| 962 |
+
Company Maintained Vehicle
|
| 963 |
+
Definition:
|
| 964 |
+
MartechSol provides company maintained cars to employees at certain designation levels. The
|
| 965 |
+
policy varies across different departments. The company maintained car will be owned and
|
| 966 |
+
maintained by the company for the use of the employee.
|
| 967 |
+
Policy:
|
| 968 |
+
Employees should drive their allotted vehicle in a safe and responsible vehicle. They must abide by
|
| 969 |
+
driving laws.
|
| 970 |
+
Scheduled maintenance will be carried out by the company after every 5,000 KMs or 3 months
|
| 971 |
+
(whichever comes first).
|
| 972 |
+
Employees are requested to keep a copy of the following documents in the company vehicle:
|
| 973 |
+
o Authority Letter
|
| 974 |
+
o Motor Vehicle Tax paper
|
| 975 |
+
o Insurance paper
|
| 976 |
+
o Copy of the first page of the vehicle’s registration book
|
| 977 |
+
Documentation & Process:
|
| 978 |
+
Prior to vehicle allocation, employee must provide a copy of valid driving license of self (or driver)
|
| 979 |
+
for company record.
|
| 980 |
+
All maintenance and wear & tear requests need to be made through submitting a ticket on portal or
|
| 981 |
+
by contacting Admin Department.
|
| 982 |
+
In case of an accident:
|
| 983 |
+
o All accidents must be reported to the Admin Department.
|
| 984 |
+
o If the company vehicle is stolen, report the theft immediately to the local police and to the
|
| 985 |
+
Admin Department. Obtain a copy of the police report filed.
|
| 986 |
+
Healthcare Insurance – For Self & Spouse
|
| 987 |
+
Health care benefits are provided to all full-time employees and their spouse. Complete details of this
|
| 988 |
+
program are provided below:
|
| 989 |
+
“ANNEXURE A”
|
| 990 |
+
PROCEDURE FOR UTILIZATION OF HOSPITALIZATION EXPENSE BENEFIT
|
| 991 |
+
Emergency Hospitalization
|
| 992 |
+
In case of emergency, approach the nearest hospital. In case it is a PANEL HOSPITAL
|
| 993 |
+
1. Identify yourself as an NJI insured.
|
| 994 |
+
2. Produce Health Card / Letter of Authority
|
| 995 |
+
3. Get the treatment
|
| 996 |
+
4. Get the treatment on credit, up to the limits available.
|
| 997 |
+
5. Pay only the amount that exceeds the entitlement, if any, before discharge.
|
| 998 |
+
|
| 999 |
+
|
| 1000 |
+
In case it is a NON-PANEL HOSPITAL
|
| 1001 |
+
1. Inform NJI within 24 hours of the hospitalization.
|
| 1002 |
+
2. Pay cash for the treatment.
|
| 1003 |
+
3. Submit all original bills /supporting documents* with our claim form for reimbursement,
|
| 1004 |
+
within 30 days of discharge from the hospital.
|
| 1005 |
+
4. Settlement of claim will be done in line of the policy terms.
|
| 1006 |
+
“ANNEXURE B”
|
| 1007 |
+
PROCEDURE FOR UTILIZATION OF HOSPITALIZATION EXPENSE BENEFIT
|
| 1008 |
+
Emergency Hospitalization
|
| 1009 |
+
If hospitalization is advised by a licensed physician
|
| 1010 |
+
In case it is a PANEL HOSPITAL
|
| 1011 |
+
1. Approach any of our panel hospital with our Letter of Authority / Health Card
|
| 1012 |
+
2. Identify yourself as an NJI insured
|
| 1013 |
+
3. Get the treatment on credit, up to the limits available.
|
| 1014 |
+
4. Pay only the amount that exceeds the entitlement, if any, before discharge.
|
| 1015 |
+
In case it is a NON-PANEL HOSPITAL
|
| 1016 |
+
1. Send us the cost estimate from the concerned doctor of the treatment with details, for prior
|
| 1017 |
+
approval.
|
| 1018 |
+
2. Get the treatment and pay cash for the treatment.
|
| 1019 |
+
3. Submit all original bills /supporting documents* including our approval letter with our claim
|
| 1020 |
+
form for reimbursement, within 30 days of discharge from the hospital.
|
| 1021 |
+
4. Settlement of claim will be done in line of prior approval given and other policy terms.
|
| 1022 |
+
*Supporting Documents:
|
| 1023 |
+
Duly completed original NJI Claim Form
|
| 1024 |
+
Original itemized bill/invoice (breakup of charged) on hospital
|
| 1025 |
+
bill-book
|
| 1026 |
+
Discharge card / clinical summary
|
| 1027 |
+
Copy of NJI card or letter of admission
|
| 1028 |
+
Diagnostic reports
|
| 1029 |
+
Doctors prescriptions
|
| 1030 |
+
Original pharmacy vouchers
|
| 1031 |
+
Original payment receipts
|
| 1032 |
+
|
| 1033 |
+
|
| 1034 |
+
JUBILEE GENERAL INSURANCE COMPANY LTD
|
| 1035 |
+
GROUP HEALTHCARE INSURANCE PROPOSAL FOR
|
| 1036 |
+
MartechSol
|
| 1037 |
+
Hospitalization & Related Benefits
|
| 1038 |
+
Plan A Plan B Plan C Plan D
|
| 1039 |
+
H&R Limits (Per Person / Per Year) Rs.500,000 Rs.400,000 Rs.300,000 Rs.150,000
|
| 1040 |
+
Room & Board (per day) Rs.16,360 Rs.6,720 Rs.6,720 Rs.1,800
|
| 1041 |
+
Per Hospitalization
|
| 1042 |
+
Pre-Hospitalization Sub Limit (Diagnosis, 30 Days 30 Days 30 Days 30 Days
|
| 1043 |
+
Consultation, & Medicines)
|
| 1044 |
+
Post-Hospitalization Sub Limit (Follow-Ups) 30 Days 30 Days 30 Days 30 Days
|
| 1045 |
+
Daycare Surgeries & Specialized
|
| 1046 |
+
Investigations In Outpatient Settings
|
| 1047 |
+
Including but not limited to:
|
| 1048 |
+
COVERED
|
| 1049 |
+
Dialysis, Cataract Surgery, MRI, CT Scan,
|
| 1050 |
+
Endoscopy, Thallium Scan, Angiography, Treatment
|
| 1051 |
+
of Fractures, Local Road Ambulance for
|
| 1052 |
+
Emergencies only, Emergency Dental Treatment
|
| 1053 |
+
due to accidental injuries within 48 hours (for pain
|
| 1054 |
+
relief only).
|
| 1055 |
+
Plan A Presidential Layer
|
| 1056 |
+
Plan B Associate Managers, Managers & Senior Managers
|
| 1057 |
+
Plan C Executives, Senior Executives & Assistant Managers
|
| 1058 |
+
Plan D Office Boys & Other Support Staff
|
| 1059 |
+
Send us the cost estimate from the concerned doctor of the treatment with details, for prior
|
| 1060 |
+
approval. Get the treatment and pay cash for the treatment.
|
| 1061 |
+
Submit all original bills /supporting documents* including our approval letter with our claim form
|
| 1062 |
+
for reimbursement, within 30 days of discharge from the hospital.
|
| 1063 |
+
Settlement of claim will be done in line of prior approval given and other policy terms.
|
| 1064 |
+
Supporting documents
|
| 1065 |
+
Itemized hospital bill
|
| 1066 |
+
Discharge card/clinical summary
|
| 1067 |
+
Diagnostic reports
|
| 1068 |
+
Prescriptions
|
| 1069 |
+
Payment receipts
|
| 1070 |
+
|
| 1071 |
+
|
| 1072 |
+
EOBI
|
| 1073 |
+
Employee Old Age Benefit Income is a benefit provided to all full-time employees upon confirmation.
|
| 1074 |
+
The sole purpose of EOBI is to provide compulsory social insurance to employees. It extends following
|
| 1075 |
+
benefits to insured persons or their survivors:
|
| 1076 |
+
• Old-Age Pension
|
| 1077 |
+
• Survivor's Pension
|
| 1078 |
+
• Invalidity Pension
|
| 1079 |
+
• Old-Age Grant
|
| 1080 |
+
RATES:
|
| 1081 |
+
• 1- Employer's Contribution @ 5 % of the worker's minimum wages (i.e. Rs. 8000) - Rs. 400/= Per
|
| 1082 |
+
Month
|
| 1083 |
+
• 2- Employee's Contribution @ 1 % of the worker's minimum wages (i.e. Rs.8000) - Rs. 80/= Per
|
| 1084 |
+
Month
|
| 1085 |
+
Provident Fund
|
| 1086 |
+
Provident Fund is an investment/ saving fund contributed to - by employees & employers, out of which a
|
| 1087 |
+
lump sum amount is provided to each employee on retirement. This benefit is provided to all full time,
|
| 1088 |
+
confirmed employees.
|
| 1089 |
+
Click Here To View PF Policies in Detail
|
| 1090 |
+
|
| 1091 |
+
|
backup_2026-05-04/floaitng-icon/KhloeSAC.lottie
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:532a144eeea5a1a6e49ab22ae4c0dc4f94e12cf8540e3514f1bc676d929eeac2
|
| 3 |
+
size 462684
|
backup_2026-05-04/get_models.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import os
|
| 3 |
+
import httpx
|
| 4 |
+
from dotenv import load_dotenv
|
| 5 |
+
|
| 6 |
+
load_dotenv()
|
| 7 |
+
|
| 8 |
+
async def main():
|
| 9 |
+
api_key = os.getenv("GROQ_API_KEY")
|
| 10 |
+
if not api_key:
|
| 11 |
+
print("No API Key")
|
| 12 |
+
return
|
| 13 |
+
url = "https://api.groq.com/openai/v1/models"
|
| 14 |
+
headers = {"Authorization": f"Bearer {api_key}"}
|
| 15 |
+
async with httpx.AsyncClient() as client:
|
| 16 |
+
resp = await client.get(url, headers=headers)
|
| 17 |
+
if resp.status_code == 200:
|
| 18 |
+
models = resp.json().get("data", [])
|
| 19 |
+
for m in models:
|
| 20 |
+
if "deepseek" in m["id"].lower() or "qwen" in m["id"].lower():
|
| 21 |
+
print(m["id"])
|
| 22 |
+
else:
|
| 23 |
+
print(resp.status_code, resp.text)
|
| 24 |
+
|
| 25 |
+
asyncio.run(main())
|
backup_2026-05-04/martech_sol_logo.jpg
ADDED
|
backup_2026-05-04/requirements.txt
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
uvicorn[standard]
|
| 3 |
+
faiss-cpu
|
| 4 |
+
sentence-transformers
|
| 5 |
+
pydantic-settings
|
| 6 |
+
pypdf
|
| 7 |
+
python-dotenv
|
| 8 |
+
numpy
|
| 9 |
+
httpx
|
| 10 |
+
gradio>=5.0.0
|
| 11 |
+
python-multipart
|
| 12 |
+
pandas
|
| 13 |
+
openpyxl
|
| 14 |
+
rank_bm25
|
backup_2026-05-04/static/addon.html
ADDED
|
@@ -0,0 +1,543 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Martechsol Assistant Widget</title>
|
| 7 |
+
<style>
|
| 8 |
+
/* Premium Design for Martechsol Assistant */
|
| 9 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap');
|
| 10 |
+
|
| 11 |
+
#martech-chat-wrapper {
|
| 12 |
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
/* Floating Action Button (FAB) */
|
| 16 |
+
#martech-chat-fab {
|
| 17 |
+
position: fixed;
|
| 18 |
+
right: 10px;
|
| 19 |
+
bottom: 25px;
|
| 20 |
+
width: auto;
|
| 21 |
+
height: auto;
|
| 22 |
+
background: transparent;
|
| 23 |
+
border-radius: 50%;
|
| 24 |
+
display: flex;
|
| 25 |
+
align-items: center;
|
| 26 |
+
justify-content: center;
|
| 27 |
+
cursor: pointer;
|
| 28 |
+
z-index: 999999;
|
| 29 |
+
transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1), background 0.3s ease, box-shadow 0.3s ease;
|
| 30 |
+
-webkit-tap-highlight-color: transparent;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
#martech-chat-fab:hover {
|
| 34 |
+
transform: scale(1.1);
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
#martech-chat-fab.is-active {
|
| 38 |
+
background: #1e293b;
|
| 39 |
+
box-shadow: 0 10px 25px rgba(30, 41, 59, 0.4);
|
| 40 |
+
transform: scale(0.8);
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
/* Chat Window Styling */
|
| 44 |
+
#martech-chat-box {
|
| 45 |
+
position: fixed;
|
| 46 |
+
right: 25px;
|
| 47 |
+
bottom: 100px;
|
| 48 |
+
width: 400px;
|
| 49 |
+
height: 550px;
|
| 50 |
+
background: #ffffff;
|
| 51 |
+
border-radius: 16px;
|
| 52 |
+
overflow: hidden;
|
| 53 |
+
display: none;
|
| 54 |
+
flex-direction: column;
|
| 55 |
+
box-shadow: 0 12px 50px rgba(0, 0, 0, 0.2);
|
| 56 |
+
z-index: 999998;
|
| 57 |
+
border: 1px solid #e2e8f0;
|
| 58 |
+
opacity: 0;
|
| 59 |
+
transform: translateY(20px);
|
| 60 |
+
transition: opacity 0.3s ease, transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
#martech-chat-box.is-visible {
|
| 64 |
+
display: flex;
|
| 65 |
+
opacity: 1;
|
| 66 |
+
transform: translateY(0);
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
#martech-chat-header {
|
| 70 |
+
background: #ffffff;
|
| 71 |
+
padding: 16px 20px;
|
| 72 |
+
border-bottom: 1px solid #f1f5f9;
|
| 73 |
+
display: flex;
|
| 74 |
+
align-items: center;
|
| 75 |
+
gap: 12px;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
@keyframes pulse-dot {
|
| 79 |
+
0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7); }
|
| 80 |
+
70% { transform: scale(1.1); box-shadow: 0 0 0 10px rgba(16, 185, 129, 0); }
|
| 81 |
+
100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); }
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
#martech-chat-header .dot {
|
| 85 |
+
width: 10px;
|
| 86 |
+
height: 10px;
|
| 87 |
+
background: #10b981;
|
| 88 |
+
border-radius: 50%;
|
| 89 |
+
animation: pulse-dot 2s infinite;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
#martech-chat-header span {
|
| 93 |
+
font-weight: 600;
|
| 94 |
+
color: #1e293b;
|
| 95 |
+
font-size: 15px;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
/* Custom Chat Area Styling */
|
| 99 |
+
#chat-messages {
|
| 100 |
+
flex: 1;
|
| 101 |
+
overflow-y: auto;
|
| 102 |
+
padding: 16px;
|
| 103 |
+
display: flex;
|
| 104 |
+
flex-direction: column;
|
| 105 |
+
gap: 12px;
|
| 106 |
+
background: #f8fafc;
|
| 107 |
+
scroll-behavior: smooth;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
/* Custom Scrollbar for messages */
|
| 111 |
+
#chat-messages::-webkit-scrollbar { width: 6px; }
|
| 112 |
+
#chat-messages::-webkit-scrollbar-track { background: transparent; }
|
| 113 |
+
#chat-messages::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 10px; }
|
| 114 |
+
|
| 115 |
+
.chat-message {
|
| 116 |
+
max-width: 85%;
|
| 117 |
+
padding: 12px 16px;
|
| 118 |
+
border-radius: 16px;
|
| 119 |
+
font-size: 14px;
|
| 120 |
+
line-height: 1.5;
|
| 121 |
+
word-wrap: break-word;
|
| 122 |
+
white-space: pre-wrap;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
.chat-message.user {
|
| 126 |
+
align-self: flex-end;
|
| 127 |
+
background: #1e293b;
|
| 128 |
+
color: white;
|
| 129 |
+
border-bottom-right-radius: 4px;
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
.chat-message.bot {
|
| 133 |
+
align-self: flex-start;
|
| 134 |
+
background: #ffffff;
|
| 135 |
+
color: #334155;
|
| 136 |
+
border-bottom-left-radius: 4px;
|
| 137 |
+
border: 1px solid #e2e8f0;
|
| 138 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.02);
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
/* Surgical Bolding & List Style */
|
| 142 |
+
.chat-message b {
|
| 143 |
+
font-weight: 800;
|
| 144 |
+
color: #0f172a;
|
| 145 |
+
}
|
| 146 |
+
.chat-message.user b {
|
| 147 |
+
color: #ffffff;
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
#chat-input-area {
|
| 151 |
+
padding: 16px;
|
| 152 |
+
background: #ffffff;
|
| 153 |
+
border-top: 1px solid #f1f5f9;
|
| 154 |
+
display: flex;
|
| 155 |
+
gap: 8px;
|
| 156 |
+
align-items: center;
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
#chat-input {
|
| 160 |
+
flex: 1;
|
| 161 |
+
border: 1px solid #e2e8f0;
|
| 162 |
+
border-radius: 20px;
|
| 163 |
+
padding: 12px 16px;
|
| 164 |
+
font-size: 14px;
|
| 165 |
+
outline: none;
|
| 166 |
+
transition: border-color 0.2s;
|
| 167 |
+
font-family: inherit;
|
| 168 |
+
box-shadow: inset 0 1px 2px rgba(0,0,0,0.02);
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
#chat-input:focus {
|
| 172 |
+
border-color: #10b981;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
#chat-input:disabled {
|
| 176 |
+
background: #f1f5f9;
|
| 177 |
+
cursor: not-allowed;
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
#chat-send {
|
| 181 |
+
background: #10b981;
|
| 182 |
+
color: white;
|
| 183 |
+
border: none;
|
| 184 |
+
width: 44px;
|
| 185 |
+
height: 44px;
|
| 186 |
+
border-radius: 50%;
|
| 187 |
+
display: flex;
|
| 188 |
+
align-items: center;
|
| 189 |
+
justify-content: center;
|
| 190 |
+
cursor: pointer;
|
| 191 |
+
transition: background 0.2s, transform 0.1s;
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
#chat-send:hover {
|
| 195 |
+
background: #059669;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
#chat-send:active {
|
| 199 |
+
transform: scale(0.95);
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
#chat-send:disabled {
|
| 203 |
+
background: #cbd5e1;
|
| 204 |
+
cursor: not-allowed;
|
| 205 |
+
transform: none;
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
/* Loading / Typing Animation */
|
| 209 |
+
.typing-indicator {
|
| 210 |
+
display: flex;
|
| 211 |
+
gap: 4px;
|
| 212 |
+
padding: 4px 2px;
|
| 213 |
+
align-items: center;
|
| 214 |
+
height: 18px;
|
| 215 |
+
}
|
| 216 |
+
.typing-indicator span {
|
| 217 |
+
width: 6px;
|
| 218 |
+
height: 6px;
|
| 219 |
+
background: #94a3b8;
|
| 220 |
+
border-radius: 50%;
|
| 221 |
+
animation: typing-dots 1.4s infinite ease-in-out both;
|
| 222 |
+
}
|
| 223 |
+
.typing-indicator span:nth-child(1) { animation-delay: -0.32s; }
|
| 224 |
+
.typing-indicator span:nth-child(2) { animation-delay: -0.16s; }
|
| 225 |
+
@keyframes typing-dots {
|
| 226 |
+
0%, 80%, 100% { transform: scale(0); }
|
| 227 |
+
40% { transform: scale(1); }
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
@media (max-width: 480px) {
|
| 231 |
+
#martech-chat-box {
|
| 232 |
+
right: 15px;
|
| 233 |
+
bottom: 100px;
|
| 234 |
+
width: calc(100vw - 30px);
|
| 235 |
+
height: 70vh;
|
| 236 |
+
}
|
| 237 |
+
}
|
| 238 |
+
</style>
|
| 239 |
+
</head>
|
| 240 |
+
<body>
|
| 241 |
+
|
| 242 |
+
<!-- Lottie Player Script -->
|
| 243 |
+
<script src="https://unpkg.com/@dotlottie/player-component@latest/dist/dotlottie-player.mjs" type="module"></script>
|
| 244 |
+
|
| 245 |
+
<div id="martech-chat-wrapper">
|
| 246 |
+
<div id="martech-chat-fab">
|
| 247 |
+
<!-- Animated Chatbot Icon -->
|
| 248 |
+
<div id="chat-icon" style="width: 233px; height: 238px; display: flex; align-items: center; justify-content: center; pointer-events: none;">
|
| 249 |
+
<dotlottie-player
|
| 250 |
+
src="https://joedown11-chatrag.hf.space/static/bot-icon.lottie"
|
| 251 |
+
background="transparent" speed="1" style="width: 100%; height: 100%; will-change: transform;" loop autoplay>
|
| 252 |
+
</dotlottie-player>
|
| 253 |
+
</div>
|
| 254 |
+
<!-- Close Icon -->
|
| 255 |
+
<svg id="close-icon" viewBox="0 0 24 24" style="display:none; width: 60px; height: 60px;">
|
| 256 |
+
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" fill="white" />
|
| 257 |
+
</svg>
|
| 258 |
+
</div>
|
| 259 |
+
|
| 260 |
+
<div id="martech-chat-box">
|
| 261 |
+
<div id="martech-chat-header">
|
| 262 |
+
<div class="dot"></div>
|
| 263 |
+
<span>Martechsol Assistant</span>
|
| 264 |
+
</div>
|
| 265 |
+
|
| 266 |
+
<!-- Custom Chat UI replacing iframe -->
|
| 267 |
+
<div id="chat-messages">
|
| 268 |
+
<div class="chat-message bot">Welcome! How can I help you today?</div>
|
| 269 |
+
</div>
|
| 270 |
+
|
| 271 |
+
<div id="chat-input-area">
|
| 272 |
+
<input type="text" id="chat-input" placeholder="Type your question here..." autocomplete="off" />
|
| 273 |
+
<button id="chat-send" title="Send">
|
| 274 |
+
<!-- Send Icon -->
|
| 275 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 276 |
+
<line x1="22" y1="2" x2="11" y2="13"></line>
|
| 277 |
+
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
|
| 278 |
+
</svg>
|
| 279 |
+
</button>
|
| 280 |
+
</div>
|
| 281 |
+
</div>
|
| 282 |
+
</div>
|
| 283 |
+
|
| 284 |
+
<script>
|
| 285 |
+
(function () {
|
| 286 |
+
// UI Elements
|
| 287 |
+
const fab = document.getElementById('martech-chat-fab');
|
| 288 |
+
const box = document.getElementById('martech-chat-box');
|
| 289 |
+
const chatIcon = document.getElementById('chat-icon');
|
| 290 |
+
const closeIcon = document.getElementById('close-icon');
|
| 291 |
+
const messagesContainer = document.getElementById('chat-messages');
|
| 292 |
+
const inputField = document.getElementById('chat-input');
|
| 293 |
+
const sendButton = document.getElementById('chat-send');
|
| 294 |
+
|
| 295 |
+
// FAB Toggle Logic
|
| 296 |
+
fab.addEventListener('click', function () {
|
| 297 |
+
const isVisible = box.classList.contains('is-visible');
|
| 298 |
+
if (isVisible) {
|
| 299 |
+
box.classList.remove('is-visible');
|
| 300 |
+
setTimeout(() => { box.style.display = 'none'; }, 300);
|
| 301 |
+
chatIcon.style.display = 'block';
|
| 302 |
+
closeIcon.style.display = 'none';
|
| 303 |
+
fab.classList.remove('is-active');
|
| 304 |
+
} else {
|
| 305 |
+
box.style.display = 'flex';
|
| 306 |
+
setTimeout(() => { box.classList.add('is-visible'); inputField.focus(); }, 10);
|
| 307 |
+
chatIcon.style.display = 'none';
|
| 308 |
+
closeIcon.style.display = 'block';
|
| 309 |
+
fab.classList.add('is-active');
|
| 310 |
+
}
|
| 311 |
+
});
|
| 312 |
+
|
| 313 |
+
// Chat Logic
|
| 314 |
+
const API_URL = "https://joedown11-chatrag.hf.space/api/chat";
|
| 315 |
+
const BASE_URL = API_URL.replace("/api/chat", "");
|
| 316 |
+
let chatHistory = [];
|
| 317 |
+
|
| 318 |
+
// ── Session ID: generate once, persist in localStorage ──
|
| 319 |
+
let sessionId = localStorage.getItem('martech_session_id');
|
| 320 |
+
if (!sessionId) {
|
| 321 |
+
// Use a temporary name/id until we get the real one
|
| 322 |
+
sessionId = 'user-' + Date.now() + '-' + Math.random().toString(36).slice(2, 9);
|
| 323 |
+
localStorage.setItem('martech_session_id', sessionId);
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
function formatMessage(text) {
|
| 327 |
+
if (!text) return "";
|
| 328 |
+
// 1. Convert Markdown Bold to HTML Bold (safety)
|
| 329 |
+
let formatted = text.replace(/\*\*(.*?)\*\*/g, '<b>$1</b>');
|
| 330 |
+
// 2. Remove the '*' at the start of lines and use a bullet
|
| 331 |
+
formatted = formatted.replace(/^\* /gm, '• ');
|
| 332 |
+
// 3. Newlines to BR
|
| 333 |
+
return formatted.replace(/\n/g, '<br>');
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
function appendMessage(role, content) {
|
| 337 |
+
const div = document.createElement('div');
|
| 338 |
+
div.className = `chat-message ${role}`;
|
| 339 |
+
div.innerHTML = formatMessage(content);
|
| 340 |
+
messagesContainer.appendChild(div);
|
| 341 |
+
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
| 342 |
+
return div;
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
// ── User Integration & History ──────────────────────────────────────────
|
| 346 |
+
async function initUserSession() {
|
| 347 |
+
// 1. Discover User Name immediately (0.2s)
|
| 348 |
+
let userName = null;
|
| 349 |
+
try {
|
| 350 |
+
const storageKeys = ['userInfo', 'user', 'profile', 'auth', 'currentUser', 'account'];
|
| 351 |
+
for (const key of storageKeys) {
|
| 352 |
+
const stored = localStorage.getItem(key) || sessionStorage.getItem(key);
|
| 353 |
+
if (stored) {
|
| 354 |
+
try {
|
| 355 |
+
const parsed = JSON.parse(stored);
|
| 356 |
+
userName = parsed.name || parsed.fullName || (parsed.user && parsed.user.name) || parsed.display_name;
|
| 357 |
+
if (userName) break;
|
| 358 |
+
} catch(err) {
|
| 359 |
+
if (typeof stored === 'string' && stored.length < 50 && !stored.includes('{')) {
|
| 360 |
+
userName = stored; break;
|
| 361 |
+
}
|
| 362 |
+
}
|
| 363 |
+
}
|
| 364 |
+
}
|
| 365 |
+
} catch (e) {}
|
| 366 |
+
|
| 367 |
+
if (!userName) {
|
| 368 |
+
try {
|
| 369 |
+
const pResp = await fetch("https://hrmmartechsol-ba71e8dc3fa2.herokuapp.com/api/users/profile", {
|
| 370 |
+
credentials: 'include'
|
| 371 |
+
});
|
| 372 |
+
if (pResp.ok) {
|
| 373 |
+
const pData = await pResp.json();
|
| 374 |
+
userName = pData.name || (pData.personalDetails && pData.personalDetails.fullName);
|
| 375 |
+
}
|
| 376 |
+
} catch (e) {}
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
// Multi-user logic: if name changed, clear old session and start fresh
|
| 380 |
+
const lastKnownName = localStorage.getItem('martech_last_user_name');
|
| 381 |
+
if (userName && lastKnownName && userName !== lastKnownName) {
|
| 382 |
+
localStorage.removeItem('martech_session_id');
|
| 383 |
+
sessionId = 'user-' + Date.now() + '-' + Math.random().toString(36).slice(2, 9);
|
| 384 |
+
localStorage.setItem('martech_session_id', sessionId);
|
| 385 |
+
messagesContainer.innerHTML = '<div class="chat-message bot">Welcome! How can I help you today?</div>';
|
| 386 |
+
}
|
| 387 |
+
if (userName) {
|
| 388 |
+
localStorage.setItem('martech_last_user_name', userName);
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
// 2. Fetch history for this session
|
| 392 |
+
try {
|
| 393 |
+
const hResp = await fetch(BASE_URL + "/api/session/history/" + sessionId);
|
| 394 |
+
if (hResp.ok) {
|
| 395 |
+
const hData = await hResp.json();
|
| 396 |
+
if (hData.history && hData.history.length > 0) {
|
| 397 |
+
messagesContainer.innerHTML = '';
|
| 398 |
+
chatHistory = hData.history;
|
| 399 |
+
chatHistory.forEach(m => {
|
| 400 |
+
appendMessage(m.role === 'assistant' ? 'bot' : 'user', m.content);
|
| 401 |
+
});
|
| 402 |
+
}
|
| 403 |
+
}
|
| 404 |
+
} catch (e) {}
|
| 405 |
+
|
| 406 |
+
// 3. Sync name with backend
|
| 407 |
+
if (userName) {
|
| 408 |
+
await fetch(BASE_URL + "/api/session/update-name", {
|
| 409 |
+
method: 'POST',
|
| 410 |
+
headers: { 'Content-Type': 'application/json' },
|
| 411 |
+
body: JSON.stringify({ session_id: sessionId, user_name: userName })
|
| 412 |
+
}).then(r => r.json()).then(rData => {
|
| 413 |
+
if (rData.new_session_id && rData.new_session_id !== sessionId) {
|
| 414 |
+
sessionId = rData.new_session_id;
|
| 415 |
+
localStorage.setItem('martech_session_id', sessionId);
|
| 416 |
+
}
|
| 417 |
+
}).catch(() => {});
|
| 418 |
+
}
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
initUserSession();
|
| 422 |
+
|
| 423 |
+
function showTypingIndicator() {
|
| 424 |
+
const div = document.createElement('div');
|
| 425 |
+
div.className = 'chat-message bot';
|
| 426 |
+
div.innerHTML = '<div class="typing-indicator"><span></span><span></span><span></span></div>';
|
| 427 |
+
messagesContainer.appendChild(div);
|
| 428 |
+
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
| 429 |
+
return div;
|
| 430 |
+
}
|
| 431 |
+
|
| 432 |
+
// Typo Configuration for Human-like Typing
|
| 433 |
+
const NEARBY_KEYS = {
|
| 434 |
+
'a': 'sqwz', 'b': 'vghn', 'c': 'xdfv', 'd': 'sfecx', 'e': 'wrsdf',
|
| 435 |
+
'f': 'dgrtcv', 'g': 'fhtybn', 'h': 'gjyunb', 'i': 'uojkl', 'j': 'hkunmi',
|
| 436 |
+
'k': 'jlomi', 'l': 'kop', 'm': 'njk', 'n': 'bhjm', 'o': 'ipkl',
|
| 437 |
+
'p': 'ol', 'q': 'wa', 'r': 'edft', 's': 'awedxz', 't': 'rfgy',
|
| 438 |
+
'u': 'yihj', 'v': 'cfgb', 'w': 'qase', 'x': 'zsdc', 'y': 'tghu',
|
| 439 |
+
'z': 'xas',
|
| 440 |
+
};
|
| 441 |
+
const TYPO_CHANCE = 0.07;
|
| 442 |
+
|
| 443 |
+
function getNearbyChar(ch) {
|
| 444 |
+
const lower = ch.toLowerCase();
|
| 445 |
+
if (NEARBY_KEYS[lower]) {
|
| 446 |
+
const options = NEARBY_KEYS[lower];
|
| 447 |
+
const replacement = options.charAt(Math.floor(Math.random() * options.length));
|
| 448 |
+
return ch === ch.toUpperCase() ? replacement.toUpperCase() : replacement;
|
| 449 |
+
}
|
| 450 |
+
const abc = "abcdefghijklmnopqrstuvwxyz";
|
| 451 |
+
return abc.charAt(Math.floor(Math.random() * abc.length));
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
| 455 |
+
|
| 456 |
+
async function typeMessageWithTypos(element, text) {
|
| 457 |
+
const fullHtml = formatMessage(text);
|
| 458 |
+
|
| 459 |
+
// Simple HTML-aware typing: split by tags
|
| 460 |
+
const tokens = fullHtml.split(/(<[^>]*>)/);
|
| 461 |
+
let currentHtml = "";
|
| 462 |
+
|
| 463 |
+
for (const token of tokens) {
|
| 464 |
+
if (token.startsWith("<")) {
|
| 465 |
+
// Instantly add tags
|
| 466 |
+
currentHtml += token;
|
| 467 |
+
element.innerHTML = currentHtml;
|
| 468 |
+
} else {
|
| 469 |
+
// Type text content char by char
|
| 470 |
+
for (let char of token) {
|
| 471 |
+
currentHtml += char;
|
| 472 |
+
element.innerHTML = currentHtml;
|
| 473 |
+
await sleep(Math.random() * 15 + 5);
|
| 474 |
+
}
|
| 475 |
+
}
|
| 476 |
+
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
| 477 |
+
}
|
| 478 |
+
}
|
| 479 |
+
|
| 480 |
+
async function handleSend() {
|
| 481 |
+
const text = inputField.value.trim();
|
| 482 |
+
if (!text) return;
|
| 483 |
+
|
| 484 |
+
// UI state lock
|
| 485 |
+
inputField.value = '';
|
| 486 |
+
inputField.disabled = true;
|
| 487 |
+
sendButton.disabled = true;
|
| 488 |
+
|
| 489 |
+
appendMessage('user', text);
|
| 490 |
+
const typingEl = showTypingIndicator();
|
| 491 |
+
|
| 492 |
+
try {
|
| 493 |
+
// Call the FastAPI Backend directly
|
| 494 |
+
const response = await fetch(API_URL, {
|
| 495 |
+
method: 'POST',
|
| 496 |
+
headers: { 'Content-Type': 'application/json' },
|
| 497 |
+
body: JSON.stringify({
|
| 498 |
+
message: text,
|
| 499 |
+
history: chatHistory,
|
| 500 |
+
session_id: sessionId
|
| 501 |
+
})
|
| 502 |
+
});
|
| 503 |
+
|
| 504 |
+
if (!response.ok) {
|
| 505 |
+
if (response.status === 429) throw new Error("Rate limit exceeded.");
|
| 506 |
+
throw new Error("Server error.");
|
| 507 |
+
}
|
| 508 |
+
|
| 509 |
+
const data = await response.json();
|
| 510 |
+
const reply = data.reply;
|
| 511 |
+
|
| 512 |
+
typingEl.remove();
|
| 513 |
+
const botEl = appendMessage('bot', '');
|
| 514 |
+
|
| 515 |
+
// Update local memory
|
| 516 |
+
chatHistory.push({ role: "user", content: text });
|
| 517 |
+
chatHistory.push({ role: "assistant", content: reply });
|
| 518 |
+
|
| 519 |
+
// Stream text out
|
| 520 |
+
await typeMessageWithTypos(botEl, reply);
|
| 521 |
+
|
| 522 |
+
} catch (error) {
|
| 523 |
+
console.error(error);
|
| 524 |
+
typingEl.remove();
|
| 525 |
+
appendMessage('bot', "⚠️ I'm sorry, I encountered an error. Please try again.");
|
| 526 |
+
} finally {
|
| 527 |
+
inputField.disabled = false;
|
| 528 |
+
sendButton.disabled = false;
|
| 529 |
+
inputField.focus();
|
| 530 |
+
}
|
| 531 |
+
}
|
| 532 |
+
|
| 533 |
+
// Event Listeners
|
| 534 |
+
sendButton.addEventListener('click', handleSend);
|
| 535 |
+
inputField.addEventListener('keypress', function(e) {
|
| 536 |
+
if (e.key === 'Enter') handleSend();
|
| 537 |
+
});
|
| 538 |
+
|
| 539 |
+
})();
|
| 540 |
+
</script>
|
| 541 |
+
|
| 542 |
+
</body>
|
| 543 |
+
</html>
|
backup_2026-05-04/static/bot-icon.lottie
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:532a144eeea5a1a6e49ab22ae4c0dc4f94e12cf8540e3514f1bc676d929eeac2
|
| 3 |
+
size 462684
|
backup_2026-05-04/static/chat-loader.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Martechsol Assistant Loader
|
| 3 |
+
* Usage: <script src="https://joedown11-chatrag.hf.space/static/chat-loader.js"></script>
|
| 4 |
+
*/
|
| 5 |
+
(function() {
|
| 6 |
+
const baseUrl = 'https://joedown11-chatrag.hf.space';
|
| 7 |
+
|
| 8 |
+
// Create container
|
| 9 |
+
const container = document.createElement('div');
|
| 10 |
+
container.id = 'martech-chat-widget-container';
|
| 11 |
+
document.body.appendChild(container);
|
| 12 |
+
|
| 13 |
+
// Fetch the addon HTML from the dedicated /widget endpoint
|
| 14 |
+
fetch(`${baseUrl}/widget?v=${Date.now()}`) // Cache busting
|
| 15 |
+
.then(response => response.text())
|
| 16 |
+
.then(html => {
|
| 17 |
+
// Create a temporary element to parse the HTML
|
| 18 |
+
const parser = new DOMParser();
|
| 19 |
+
const doc = parser.parseFromString(html, 'text/html');
|
| 20 |
+
|
| 21 |
+
// 1. Extract and inject styles
|
| 22 |
+
doc.querySelectorAll('style').forEach(st => {
|
| 23 |
+
document.head.appendChild(st.cloneNode(true));
|
| 24 |
+
});
|
| 25 |
+
|
| 26 |
+
// 2. Extract and inject body content
|
| 27 |
+
const wrapper = doc.querySelector('#martech-chat-wrapper');
|
| 28 |
+
if (wrapper) {
|
| 29 |
+
container.innerHTML = wrapper.outerHTML;
|
| 30 |
+
} else {
|
| 31 |
+
// Fallback: just take the body content
|
| 32 |
+
container.innerHTML = doc.body.innerHTML;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
// 3. Extract and execute scripts (critical for logic)
|
| 36 |
+
doc.querySelectorAll('script').forEach(oldScript => {
|
| 37 |
+
const newScript = document.createElement('script');
|
| 38 |
+
Array.from(oldScript.attributes).forEach(attr => {
|
| 39 |
+
newScript.setAttribute(attr.name, attr.value);
|
| 40 |
+
});
|
| 41 |
+
newScript.innerHTML = oldScript.innerHTML;
|
| 42 |
+
document.body.appendChild(newScript);
|
| 43 |
+
});
|
| 44 |
+
|
| 45 |
+
console.log("Martechsol Assistant Loaded Successfully");
|
| 46 |
+
})
|
| 47 |
+
.catch(err => console.error("Failed to load Martechsol Assistant:", err));
|
| 48 |
+
})();
|
backup_2026-05-04/test_groq.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import os
|
| 3 |
+
import httpx
|
| 4 |
+
from dotenv import load_dotenv
|
| 5 |
+
|
| 6 |
+
load_dotenv()
|
| 7 |
+
|
| 8 |
+
async def main():
|
| 9 |
+
api_key = os.getenv("GROQ_API_KEY")
|
| 10 |
+
url = "https://api.groq.com/openai/v1/chat/completions"
|
| 11 |
+
headers = {
|
| 12 |
+
"Authorization": f"Bearer {api_key}",
|
| 13 |
+
"Content-Type": "application/json"
|
| 14 |
+
}
|
| 15 |
+
payload = {
|
| 16 |
+
"model": "deepseek-r1-distill-llama-70b",
|
| 17 |
+
"temperature": 0.0,
|
| 18 |
+
"max_tokens": 1200,
|
| 19 |
+
"messages": [{"role": "system", "content": "hello"}, {"role": "user", "content": "test"}]
|
| 20 |
+
}
|
| 21 |
+
async with httpx.AsyncClient() as client:
|
| 22 |
+
resp = await client.post(url, headers=headers, json=payload)
|
| 23 |
+
print(resp.status_code)
|
| 24 |
+
print(resp.text)
|
| 25 |
+
|
| 26 |
+
asyncio.run(main())
|
backup_2026-05-04/wordpress code/WP_INTEGRATION_SCRIPT.txt
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!--
|
| 2 |
+
Martechsol Chatbot Integration Script
|
| 3 |
+
Paste this snippet right before the closing </body> tag on your WordPress site.
|
| 4 |
+
It will automatically load the UI and logic from your Hugging Face space.
|
| 5 |
+
-->
|
| 6 |
+
|
| 7 |
+
<script>
|
| 8 |
+
fetch('https://joedown11-chatrag.hf.space/static/addon.html').then(r => r.text()).then(h => {
|
| 9 |
+
const d = document.createElement('div'); d.innerHTML = h; document.body.appendChild(d);
|
| 10 |
+
d.querySelectorAll('script').forEach(s => { const n = document.createElement('script'); Array.from(s.attributes).forEach(a => n.setAttribute(a.name, a.value)); n.innerHTML = s.innerHTML; document.body.appendChild(n); });
|
| 11 |
+
});
|
| 12 |
+
</script>
|
backup_2026-05-04/wordpress code/addon.html
ADDED
|
@@ -0,0 +1,477 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Martechsol Assistant Widget</title>
|
| 7 |
+
<style>
|
| 8 |
+
/* Premium Design for Martechsol Assistant */
|
| 9 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap');
|
| 10 |
+
|
| 11 |
+
#martech-chat-wrapper {
|
| 12 |
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
/* Floating Action Button (FAB) */
|
| 16 |
+
#martech-chat-fab {
|
| 17 |
+
position: fixed;
|
| 18 |
+
right: 10px;
|
| 19 |
+
bottom: 25px;
|
| 20 |
+
width: auto;
|
| 21 |
+
height: auto;
|
| 22 |
+
background: transparent;
|
| 23 |
+
border-radius: 50%;
|
| 24 |
+
display: flex;
|
| 25 |
+
align-items: center;
|
| 26 |
+
justify-content: center;
|
| 27 |
+
cursor: pointer;
|
| 28 |
+
z-index: 999999;
|
| 29 |
+
transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1), background 0.3s ease, box-shadow 0.3s ease;
|
| 30 |
+
-webkit-tap-highlight-color: transparent;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
#martech-chat-fab:hover {
|
| 34 |
+
transform: scale(1.1);
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
#martech-chat-fab.is-active {
|
| 38 |
+
background: #1e293b;
|
| 39 |
+
box-shadow: 0 10px 25px rgba(30, 41, 59, 0.4);
|
| 40 |
+
transform: scale(0.8);
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
/* Chat Window Styling */
|
| 44 |
+
#martech-chat-box {
|
| 45 |
+
position: fixed;
|
| 46 |
+
right: 25px;
|
| 47 |
+
bottom: 100px;
|
| 48 |
+
width: 400px;
|
| 49 |
+
height: 550px;
|
| 50 |
+
background: #ffffff;
|
| 51 |
+
border-radius: 16px;
|
| 52 |
+
overflow: hidden;
|
| 53 |
+
display: none;
|
| 54 |
+
flex-direction: column;
|
| 55 |
+
box-shadow: 0 12px 50px rgba(0, 0, 0, 0.2);
|
| 56 |
+
z-index: 999998;
|
| 57 |
+
border: 1px solid #e2e8f0;
|
| 58 |
+
opacity: 0;
|
| 59 |
+
transform: translateY(20px);
|
| 60 |
+
transition: opacity 0.3s ease, transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
#martech-chat-box.is-visible {
|
| 64 |
+
display: flex;
|
| 65 |
+
opacity: 1;
|
| 66 |
+
transform: translateY(0);
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
#martech-chat-header {
|
| 70 |
+
background: #ffffff;
|
| 71 |
+
padding: 16px 20px;
|
| 72 |
+
border-bottom: 1px solid #f1f5f9;
|
| 73 |
+
display: flex;
|
| 74 |
+
align-items: center;
|
| 75 |
+
gap: 12px;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
@keyframes pulse-dot {
|
| 79 |
+
0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7); }
|
| 80 |
+
70% { transform: scale(1.1); box-shadow: 0 0 0 10px rgba(16, 185, 129, 0); }
|
| 81 |
+
100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); }
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
#martech-chat-header .dot {
|
| 85 |
+
width: 10px;
|
| 86 |
+
height: 10px;
|
| 87 |
+
background: #10b981;
|
| 88 |
+
border-radius: 50%;
|
| 89 |
+
animation: pulse-dot 2s infinite;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
#martech-chat-header span {
|
| 93 |
+
font-weight: 600;
|
| 94 |
+
color: #1e293b;
|
| 95 |
+
font-size: 15px;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
/* Custom Chat Area Styling */
|
| 99 |
+
#chat-messages {
|
| 100 |
+
flex: 1;
|
| 101 |
+
overflow-y: auto;
|
| 102 |
+
padding: 16px;
|
| 103 |
+
display: flex;
|
| 104 |
+
flex-direction: column;
|
| 105 |
+
gap: 12px;
|
| 106 |
+
background: #f8fafc;
|
| 107 |
+
scroll-behavior: smooth;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
/* Custom Scrollbar for messages */
|
| 111 |
+
#chat-messages::-webkit-scrollbar { width: 6px; }
|
| 112 |
+
#chat-messages::-webkit-scrollbar-track { background: transparent; }
|
| 113 |
+
#chat-messages::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 10px; }
|
| 114 |
+
|
| 115 |
+
.chat-message {
|
| 116 |
+
max-width: 85%;
|
| 117 |
+
padding: 12px 16px;
|
| 118 |
+
border-radius: 16px;
|
| 119 |
+
font-size: 14px;
|
| 120 |
+
line-height: 1.5;
|
| 121 |
+
word-wrap: break-word;
|
| 122 |
+
white-space: pre-wrap;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
.chat-message.user {
|
| 126 |
+
align-self: flex-end;
|
| 127 |
+
background: #1e293b;
|
| 128 |
+
color: white;
|
| 129 |
+
border-bottom-right-radius: 4px;
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
.chat-message.bot {
|
| 133 |
+
align-self: flex-start;
|
| 134 |
+
background: #ffffff;
|
| 135 |
+
color: #334155;
|
| 136 |
+
border-bottom-left-radius: 4px;
|
| 137 |
+
border: 1px solid #e2e8f0;
|
| 138 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.02);
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
#chat-input-area {
|
| 142 |
+
padding: 16px;
|
| 143 |
+
background: #ffffff;
|
| 144 |
+
border-top: 1px solid #f1f5f9;
|
| 145 |
+
display: flex;
|
| 146 |
+
gap: 8px;
|
| 147 |
+
align-items: center;
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
#chat-input {
|
| 151 |
+
flex: 1;
|
| 152 |
+
border: 1px solid #e2e8f0;
|
| 153 |
+
border-radius: 20px;
|
| 154 |
+
padding: 12px 16px;
|
| 155 |
+
font-size: 14px;
|
| 156 |
+
outline: none;
|
| 157 |
+
transition: border-color 0.2s;
|
| 158 |
+
font-family: inherit;
|
| 159 |
+
box-shadow: inset 0 1px 2px rgba(0,0,0,0.02);
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
#chat-input:focus {
|
| 163 |
+
border-color: #10b981;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
#chat-input:disabled {
|
| 167 |
+
background: #f1f5f9;
|
| 168 |
+
cursor: not-allowed;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
#chat-send {
|
| 172 |
+
background: #10b981;
|
| 173 |
+
color: white;
|
| 174 |
+
border: none;
|
| 175 |
+
width: 44px;
|
| 176 |
+
height: 44px;
|
| 177 |
+
border-radius: 50%;
|
| 178 |
+
display: flex;
|
| 179 |
+
align-items: center;
|
| 180 |
+
justify-content: center;
|
| 181 |
+
cursor: pointer;
|
| 182 |
+
transition: background 0.2s, transform 0.1s;
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
#chat-send:hover {
|
| 186 |
+
background: #059669;
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
#chat-send:active {
|
| 190 |
+
transform: scale(0.95);
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
#chat-send:disabled {
|
| 194 |
+
background: #cbd5e1;
|
| 195 |
+
cursor: not-allowed;
|
| 196 |
+
transform: none;
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
/* Loading / Typing Animation */
|
| 200 |
+
.typing-indicator {
|
| 201 |
+
display: flex;
|
| 202 |
+
gap: 4px;
|
| 203 |
+
padding: 4px 2px;
|
| 204 |
+
align-items: center;
|
| 205 |
+
height: 18px;
|
| 206 |
+
}
|
| 207 |
+
.typing-indicator span {
|
| 208 |
+
width: 6px;
|
| 209 |
+
height: 6px;
|
| 210 |
+
background: #94a3b8;
|
| 211 |
+
border-radius: 50%;
|
| 212 |
+
animation: typing-dots 1.4s infinite ease-in-out both;
|
| 213 |
+
}
|
| 214 |
+
.typing-indicator span:nth-child(1) { animation-delay: -0.32s; }
|
| 215 |
+
.typing-indicator span:nth-child(2) { animation-delay: -0.16s; }
|
| 216 |
+
@keyframes typing-dots {
|
| 217 |
+
0%, 80%, 100% { transform: scale(0); }
|
| 218 |
+
40% { transform: scale(1); }
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
@media (max-width: 480px) {
|
| 222 |
+
#martech-chat-box {
|
| 223 |
+
right: 15px;
|
| 224 |
+
bottom: 100px;
|
| 225 |
+
width: calc(100vw - 30px);
|
| 226 |
+
height: 70vh;
|
| 227 |
+
}
|
| 228 |
+
}
|
| 229 |
+
</style>
|
| 230 |
+
</head>
|
| 231 |
+
<body>
|
| 232 |
+
|
| 233 |
+
<!-- Lottie Player Script -->
|
| 234 |
+
<script src="https://unpkg.com/@dotlottie/player-component@latest/dist/dotlottie-player.mjs" type="module"></script>
|
| 235 |
+
|
| 236 |
+
<div id="martech-chat-wrapper">
|
| 237 |
+
<div id="martech-chat-fab">
|
| 238 |
+
<!-- Animated Chatbot Icon -->
|
| 239 |
+
<div id="chat-icon" style="width: 233px; height: 238px; display: flex; align-items: center; justify-content: center; pointer-events: none;">
|
| 240 |
+
<dotlottie-player
|
| 241 |
+
src="https://seashell-cattle-543014.hostingersite.com/wp-content/uploads/2026/04/KhloeSAC.lottie"
|
| 242 |
+
background="transparent" speed="1" style="width: 100%; height: 100%; will-change: transform;" loop autoplay>
|
| 243 |
+
</dotlottie-player>
|
| 244 |
+
</div>
|
| 245 |
+
<!-- Close Icon -->
|
| 246 |
+
<svg id="close-icon" viewBox="0 0 24 24" style="display:none; width: 60px; height: 60px;">
|
| 247 |
+
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" fill="white" />
|
| 248 |
+
</svg>
|
| 249 |
+
</div>
|
| 250 |
+
|
| 251 |
+
<div id="martech-chat-box">
|
| 252 |
+
<div id="martech-chat-header">
|
| 253 |
+
<div class="dot"></div>
|
| 254 |
+
<span>Martechsol Assistant</span>
|
| 255 |
+
</div>
|
| 256 |
+
|
| 257 |
+
<!-- Custom Chat UI replacing iframe -->
|
| 258 |
+
<div id="chat-messages">
|
| 259 |
+
<div class="chat-message bot">Welcome! How can I help you today?</div>
|
| 260 |
+
</div>
|
| 261 |
+
|
| 262 |
+
<div id="chat-input-area">
|
| 263 |
+
<input type="text" id="chat-input" placeholder="Type your question here..." autocomplete="off" />
|
| 264 |
+
<button id="chat-send" title="Send">
|
| 265 |
+
<!-- Send Icon -->
|
| 266 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 267 |
+
<line x1="22" y1="2" x2="11" y2="13"></line>
|
| 268 |
+
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
|
| 269 |
+
</svg>
|
| 270 |
+
</button>
|
| 271 |
+
</div>
|
| 272 |
+
</div>
|
| 273 |
+
</div>
|
| 274 |
+
|
| 275 |
+
<script>
|
| 276 |
+
(function () {
|
| 277 |
+
// UI Elements
|
| 278 |
+
const fab = document.getElementById('martech-chat-fab');
|
| 279 |
+
const box = document.getElementById('martech-chat-box');
|
| 280 |
+
const chatIcon = document.getElementById('chat-icon');
|
| 281 |
+
const closeIcon = document.getElementById('close-icon');
|
| 282 |
+
const messagesContainer = document.getElementById('chat-messages');
|
| 283 |
+
const inputField = document.getElementById('chat-input');
|
| 284 |
+
const sendButton = document.getElementById('chat-send');
|
| 285 |
+
|
| 286 |
+
// FAB Toggle Logic
|
| 287 |
+
fab.addEventListener('click', function () {
|
| 288 |
+
const isVisible = box.classList.contains('is-visible');
|
| 289 |
+
if (isVisible) {
|
| 290 |
+
box.classList.remove('is-visible');
|
| 291 |
+
setTimeout(() => { box.style.display = 'none'; }, 300);
|
| 292 |
+
chatIcon.style.display = 'block';
|
| 293 |
+
closeIcon.style.display = 'none';
|
| 294 |
+
fab.classList.remove('is-active');
|
| 295 |
+
} else {
|
| 296 |
+
box.style.display = 'flex';
|
| 297 |
+
setTimeout(() => { box.classList.add('is-visible'); inputField.focus(); }, 10);
|
| 298 |
+
chatIcon.style.display = 'none';
|
| 299 |
+
closeIcon.style.display = 'block';
|
| 300 |
+
fab.classList.add('is-active');
|
| 301 |
+
}
|
| 302 |
+
});
|
| 303 |
+
|
| 304 |
+
// Chat Logic
|
| 305 |
+
const API_URL = "https://joedown11-chatrag.hf.space/api/chat";
|
| 306 |
+
let chatHistory = [];
|
| 307 |
+
|
| 308 |
+
// ── Session ID: generate once, persist in localStorage ──
|
| 309 |
+
let sessionId = localStorage.getItem('martech_session_id');
|
| 310 |
+
if (!sessionId) {
|
| 311 |
+
sessionId = 'sess-' + Date.now() + '-' + Math.random().toString(36).slice(2, 9);
|
| 312 |
+
localStorage.setItem('martech_session_id', sessionId);
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
function appendMessage(role, content) {
|
| 316 |
+
const div = document.createElement('div');
|
| 317 |
+
div.className = `chat-message ${role}`;
|
| 318 |
+
div.textContent = content;
|
| 319 |
+
messagesContainer.appendChild(div);
|
| 320 |
+
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
| 321 |
+
return div;
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
function showTypingIndicator() {
|
| 325 |
+
const div = document.createElement('div');
|
| 326 |
+
div.className = 'chat-message bot';
|
| 327 |
+
div.innerHTML = '<div class="typing-indicator"><span></span><span></span><span></span></div>';
|
| 328 |
+
messagesContainer.appendChild(div);
|
| 329 |
+
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
| 330 |
+
return div;
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
// Typo Configuration for Human-like Typing
|
| 334 |
+
const NEARBY_KEYS = {
|
| 335 |
+
'a': 'sqwz', 'b': 'vghn', 'c': 'xdfv', 'd': 'sfecx', 'e': 'wrsdf',
|
| 336 |
+
'f': 'dgrtcv', 'g': 'fhtybn', 'h': 'gjyunb', 'i': 'uojkl', 'j': 'hkunmi',
|
| 337 |
+
'k': 'jlomi', 'l': 'kop', 'm': 'njk', 'n': 'bhjm', 'o': 'ipkl',
|
| 338 |
+
'p': 'ol', 'q': 'wa', 'r': 'edft', 's': 'awedxz', 't': 'rfgy',
|
| 339 |
+
'u': 'yihj', 'v': 'cfgb', 'w': 'qase', 'x': 'zsdc', 'y': 'tghu',
|
| 340 |
+
'z': 'xas',
|
| 341 |
+
};
|
| 342 |
+
const TYPO_CHANCE = 0.07;
|
| 343 |
+
|
| 344 |
+
function getNearbyChar(ch) {
|
| 345 |
+
const lower = ch.toLowerCase();
|
| 346 |
+
if (NEARBY_KEYS[lower]) {
|
| 347 |
+
const options = NEARBY_KEYS[lower];
|
| 348 |
+
const replacement = options.charAt(Math.floor(Math.random() * options.length));
|
| 349 |
+
return ch === ch.toUpperCase() ? replacement.toUpperCase() : replacement;
|
| 350 |
+
}
|
| 351 |
+
const abc = "abcdefghijklmnopqrstuvwxyz";
|
| 352 |
+
return abc.charAt(Math.floor(Math.random() * abc.length));
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
| 356 |
+
|
| 357 |
+
async function typeMessageWithTypos(element, text) {
|
| 358 |
+
let displayed = "";
|
| 359 |
+
const words = text.split(" ");
|
| 360 |
+
|
| 361 |
+
for (let i = 0; i < words.length; i++) {
|
| 362 |
+
const word = words[i];
|
| 363 |
+
const separator = i > 0 ? " " : "";
|
| 364 |
+
const isAlpha = /^[a-zA-Z]+$/.test(word);
|
| 365 |
+
|
| 366 |
+
// Randomly insert a typo
|
| 367 |
+
if (i > 0 && word.length > 4 && Math.random() < TYPO_CHANCE && isAlpha) {
|
| 368 |
+
const typoPos = Math.floor(Math.random() * (word.length - 2)) + 2;
|
| 369 |
+
|
| 370 |
+
// 1. Type correct part
|
| 371 |
+
for (let char of (separator + word.substring(0, typoPos))) {
|
| 372 |
+
displayed += char;
|
| 373 |
+
element.textContent = displayed;
|
| 374 |
+
await sleep(Math.random() * 40 + 30);
|
| 375 |
+
}
|
| 376 |
+
// 2. Type wrong char
|
| 377 |
+
const wrongChar = getNearbyChar(word[typoPos]);
|
| 378 |
+
displayed += wrongChar;
|
| 379 |
+
element.textContent = displayed;
|
| 380 |
+
await sleep(Math.random() * 150 + 150); // Realize error
|
| 381 |
+
|
| 382 |
+
// 3. Backspace
|
| 383 |
+
await sleep(Math.random() * 200 + 200);
|
| 384 |
+
displayed = displayed.slice(0, -1);
|
| 385 |
+
element.textContent = displayed;
|
| 386 |
+
await sleep(Math.random() * 100 + 100);
|
| 387 |
+
|
| 388 |
+
// 4. Type rest
|
| 389 |
+
for (let char of word.substring(typoPos)) {
|
| 390 |
+
displayed += char;
|
| 391 |
+
element.textContent = displayed;
|
| 392 |
+
await sleep(Math.random() * 40 + 40);
|
| 393 |
+
}
|
| 394 |
+
} else {
|
| 395 |
+
// Type normally
|
| 396 |
+
for (let char of (separator + word)) {
|
| 397 |
+
displayed += char;
|
| 398 |
+
element.textContent = displayed;
|
| 399 |
+
await sleep(char === " " ? Math.random() * 70 + 80 : Math.random() * 40 + 20);
|
| 400 |
+
}
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
| 404 |
+
|
| 405 |
+
// Punctuation pauses
|
| 406 |
+
if (word && [".", "!", "?"].includes(word[word.length - 1])) {
|
| 407 |
+
await sleep(Math.random() * 400 + 400);
|
| 408 |
+
} else if (word && [",", ":", ";"].includes(word[word.length - 1])) {
|
| 409 |
+
await sleep(Math.random() * 200 + 200);
|
| 410 |
+
}
|
| 411 |
+
}
|
| 412 |
+
}
|
| 413 |
+
|
| 414 |
+
async function handleSend() {
|
| 415 |
+
const text = inputField.value.trim();
|
| 416 |
+
if (!text) return;
|
| 417 |
+
|
| 418 |
+
// UI state lock
|
| 419 |
+
inputField.value = '';
|
| 420 |
+
inputField.disabled = true;
|
| 421 |
+
sendButton.disabled = true;
|
| 422 |
+
|
| 423 |
+
appendMessage('user', text);
|
| 424 |
+
const typingEl = showTypingIndicator();
|
| 425 |
+
|
| 426 |
+
try {
|
| 427 |
+
// Call the FastAPI Backend directly
|
| 428 |
+
const response = await fetch(API_URL, {
|
| 429 |
+
method: 'POST',
|
| 430 |
+
headers: { 'Content-Type': 'application/json' },
|
| 431 |
+
body: JSON.stringify({
|
| 432 |
+
message: text,
|
| 433 |
+
history: chatHistory,
|
| 434 |
+
session_id: sessionId
|
| 435 |
+
})
|
| 436 |
+
});
|
| 437 |
+
|
| 438 |
+
if (!response.ok) {
|
| 439 |
+
if (response.status === 429) throw new Error("Rate limit exceeded.");
|
| 440 |
+
throw new Error("Server error.");
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
const data = await response.json();
|
| 444 |
+
const reply = data.reply;
|
| 445 |
+
|
| 446 |
+
typingEl.remove();
|
| 447 |
+
const botEl = appendMessage('bot', '');
|
| 448 |
+
|
| 449 |
+
// Update local memory
|
| 450 |
+
chatHistory.push({ role: "user", content: text });
|
| 451 |
+
chatHistory.push({ role: "assistant", content: reply });
|
| 452 |
+
|
| 453 |
+
// Stream text out
|
| 454 |
+
await typeMessageWithTypos(botEl, reply);
|
| 455 |
+
|
| 456 |
+
} catch (error) {
|
| 457 |
+
console.error(error);
|
| 458 |
+
typingEl.remove();
|
| 459 |
+
appendMessage('bot', "⚠️ I'm sorry, I encountered an error. Please try again.");
|
| 460 |
+
} finally {
|
| 461 |
+
inputField.disabled = false;
|
| 462 |
+
sendButton.disabled = false;
|
| 463 |
+
inputField.focus();
|
| 464 |
+
}
|
| 465 |
+
}
|
| 466 |
+
|
| 467 |
+
// Event Listeners
|
| 468 |
+
sendButton.addEventListener('click', handleSend);
|
| 469 |
+
inputField.addEventListener('keypress', function(e) {
|
| 470 |
+
if (e.key === 'Enter') handleSend();
|
| 471 |
+
});
|
| 472 |
+
|
| 473 |
+
})();
|
| 474 |
+
</script>
|
| 475 |
+
|
| 476 |
+
</body>
|
| 477 |
+
</html>
|
backup_2026-05-04/wordpress code/addon_backup.html
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<style>
|
| 2 |
+
/* Premium Design for Martechsol Assistant */
|
| 3 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap');
|
| 4 |
+
|
| 5 |
+
#martech-chat-wrapper {
|
| 6 |
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
/* Floating Action Button (FAB) */
|
| 10 |
+
#martech-chat-fab {
|
| 11 |
+
position: fixed;
|
| 12 |
+
right: 10px;
|
| 13 |
+
bottom: 25px;
|
| 14 |
+
width: auto;
|
| 15 |
+
height: auto;
|
| 16 |
+
background: transparent;
|
| 17 |
+
border-radius: 50%;
|
| 18 |
+
display: flex;
|
| 19 |
+
align-items: center;
|
| 20 |
+
justify-content: center;
|
| 21 |
+
cursor: pointer;
|
| 22 |
+
z-index: 999999;
|
| 23 |
+
transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1), background 0.3s ease, box-shadow 0.3s ease;
|
| 24 |
+
-webkit-tap-highlight-color: transparent;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
#martech-chat-fab:hover {
|
| 28 |
+
transform: scale(1.1);
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
#martech-chat-fab.is-active {
|
| 32 |
+
background: #1e293b;
|
| 33 |
+
box-shadow: 0 10px 25px rgba(30, 41, 59, 0.4);
|
| 34 |
+
transform: scale(0.8);
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
/* Chat Window Styling */
|
| 38 |
+
#martech-chat-box {
|
| 39 |
+
position: fixed;
|
| 40 |
+
right: 25px;
|
| 41 |
+
bottom: 100px;
|
| 42 |
+
width: 400px;
|
| 43 |
+
height: 550px;
|
| 44 |
+
background: #ffffff;
|
| 45 |
+
border-radius: 16px;
|
| 46 |
+
overflow: hidden;
|
| 47 |
+
display: none;
|
| 48 |
+
flex-direction: column;
|
| 49 |
+
box-shadow: 0 12px 50px rgba(0, 0, 0, 0.2);
|
| 50 |
+
z-index: 999998;
|
| 51 |
+
border: 1px solid #e2e8f0;
|
| 52 |
+
opacity: 0;
|
| 53 |
+
transform: translateY(20px);
|
| 54 |
+
transition: opacity 0.3s ease, transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
#martech-chat-box.is-visible {
|
| 58 |
+
display: flex;
|
| 59 |
+
opacity: 1;
|
| 60 |
+
transform: translateY(0);
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
#martech-chat-header {
|
| 64 |
+
background: #ffffff;
|
| 65 |
+
padding: 16px 20px;
|
| 66 |
+
border-bottom: 1px solid #f1f5f9;
|
| 67 |
+
display: flex;
|
| 68 |
+
align-items: center;
|
| 69 |
+
gap: 12px;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
@keyframes pulse-dot {
|
| 73 |
+
0% {
|
| 74 |
+
transform: scale(1);
|
| 75 |
+
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7);
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
70% {
|
| 79 |
+
transform: scale(1.1);
|
| 80 |
+
box-shadow: 0 0 0 10px rgba(16, 185, 129, 0);
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
100% {
|
| 84 |
+
transform: scale(1);
|
| 85 |
+
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0);
|
| 86 |
+
}
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
#martech-chat-header .dot {
|
| 90 |
+
width: 10px;
|
| 91 |
+
height: 10px;
|
| 92 |
+
background: #10b981;
|
| 93 |
+
border-radius: 50%;
|
| 94 |
+
animation: pulse-dot 2s infinite;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
#martech-chat-header span {
|
| 98 |
+
font-weight: 600;
|
| 99 |
+
color: #1e293b;
|
| 100 |
+
font-size: 15px;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
#martech-chat-box iframe {
|
| 104 |
+
width: 100%;
|
| 105 |
+
flex: 1;
|
| 106 |
+
border: 0;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
@media (max-width: 480px) {
|
| 110 |
+
#martech-chat-box {
|
| 111 |
+
right: 15px;
|
| 112 |
+
bottom: 100px;
|
| 113 |
+
width: calc(100vw - 30px);
|
| 114 |
+
height: 70vh;
|
| 115 |
+
}
|
| 116 |
+
}
|
| 117 |
+
</style>
|
| 118 |
+
|
| 119 |
+
<!-- Lottie Player Script -->
|
| 120 |
+
<script src="https://unpkg.com/@dotlottie/player-component@latest/dist/dotlottie-player.mjs" type="module"></script>
|
| 121 |
+
|
| 122 |
+
<div id="martech-chat-wrapper">
|
| 123 |
+
<div id="martech-chat-fab">
|
| 124 |
+
<!-- Animated Chatbot Icon -->
|
| 125 |
+
<div id="chat-icon"
|
| 126 |
+
style="width: 233px; height: 238px; display: flex; align-items: center; justify-content: center; pointer-events: none;">
|
| 127 |
+
<dotlottie-player
|
| 128 |
+
src="https://seashell-cattle-543014.hostingersite.com/wp-content/uploads/2026/04/KhloeSAC.lottie"
|
| 129 |
+
background="transparent" speed="1" style="width: 100%; height: 100%; will-change: transform;" loop
|
| 130 |
+
autoplay>
|
| 131 |
+
</dotlottie-player>
|
| 132 |
+
</div>
|
| 133 |
+
<!-- Close Icon -->
|
| 134 |
+
<svg id="close-icon" viewBox="0 0 24 24" style="display:none; width: 60px; height: 60px;">
|
| 135 |
+
<path
|
| 136 |
+
d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"
|
| 137 |
+
fill="white" />
|
| 138 |
+
</svg>
|
| 139 |
+
</div>
|
| 140 |
+
|
| 141 |
+
<div id="martech-chat-box">
|
| 142 |
+
<div id="martech-chat-header">
|
| 143 |
+
<div class="dot"></div>
|
| 144 |
+
<span>Martechsol Assistant</span>
|
| 145 |
+
</div>
|
| 146 |
+
<iframe src="https://joedown11-chatrag.hf.space" allow="clipboard-write; microphone" loading="lazy"
|
| 147 |
+
referrerpolicy="no-referrer-when-downgrade"></iframe>
|
| 148 |
+
</div>
|
| 149 |
+
</div>
|
| 150 |
+
|
| 151 |
+
<script>
|
| 152 |
+
(function () {
|
| 153 |
+
const fab = document.getElementById('martech-chat-fab');
|
| 154 |
+
const box = document.getElementById('martech-chat-box');
|
| 155 |
+
const chatIcon = document.getElementById('chat-icon');
|
| 156 |
+
const closeIcon = document.getElementById('close-icon');
|
| 157 |
+
|
| 158 |
+
fab.addEventListener('click', function () {
|
| 159 |
+
const isVisible = box.classList.contains('is-visible');
|
| 160 |
+
if (isVisible) {
|
| 161 |
+
box.classList.remove('is-visible');
|
| 162 |
+
setTimeout(() => { box.style.display = 'none'; }, 300);
|
| 163 |
+
chatIcon.style.display = 'block';
|
| 164 |
+
closeIcon.style.display = 'none';
|
| 165 |
+
fab.classList.remove('is-active');
|
| 166 |
+
} else {
|
| 167 |
+
box.style.display = 'flex';
|
| 168 |
+
setTimeout(() => { box.classList.add('is-visible'); }, 10);
|
| 169 |
+
chatIcon.style.display = 'none';
|
| 170 |
+
closeIcon.style.display = 'block';
|
| 171 |
+
fab.classList.add('is-active');
|
| 172 |
+
}
|
| 173 |
+
});
|
| 174 |
+
})();
|
| 175 |
+
</script>
|
backup_2026-05-13_pre_intelligent_retrieval/.env.example
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
LLM_PROVIDER=fireworks
|
| 2 |
+
FIREWORKS_API_KEY=your_fireworks_api_key
|
| 3 |
+
FIREWORKS_MODEL=accounts/fireworks/models/qwen3-32b
|
| 4 |
+
FIREWORKS_REWRITE_MODEL=accounts/fireworks/models/llama-v3p1-8b-instruct
|
| 5 |
+
GROQ_API_KEY=your_groq_api_key
|
| 6 |
+
GROQ_MODEL=qwen/qwen3-32b
|
| 7 |
+
GROQ_REWRITE_MODEL=llama-3.1-8b-instant
|
| 8 |
+
HF_API_KEY=your_huggingface_api_key
|
| 9 |
+
HF_MODEL=meta-llama/Llama-3.1-8B-Instruct
|
| 10 |
+
EMBEDDING_MODEL=BAAI/bge-small-en-v1.5
|
| 11 |
+
DOCS_DIR=docs
|
| 12 |
+
INDEX_DIR=data/index
|
| 13 |
+
SESSIONS_DIR=data/sessions
|
| 14 |
+
TOP_K=4
|
| 15 |
+
CORS_ALLOW_ORIGINS=*
|
| 16 |
+
API_KEY=
|
| 17 |
+
RATE_LIMIT_REQUESTS=60
|
| 18 |
+
RATE_LIMIT_WINDOW_SECONDS=60
|
| 19 |
+
RAG_API_URL=
|
backup_2026-05-13_pre_intelligent_retrieval/.gitattributes
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.lottie filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.pdf filter=lfs diff=lfs merge=lfs -text
|
backup_2026-05-13_pre_intelligent_retrieval/.gitignore
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.env
|
| 2 |
+
__pycache__/
|
| 3 |
+
.pytest_cache/
|
| 4 |
+
.mypy_cache/
|
| 5 |
+
.venv/
|
| 6 |
+
venv/
|
| 7 |
+
# Ignoring data directory to prevent pushing binary index files
|
| 8 |
+
data/index/
|
| 9 |
+
data/sessions/
|
backup_2026-05-13_pre_intelligent_retrieval/CONFIG_NOTES.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Configuration Backup - Martechsol RAG Chatbot
|
| 2 |
+
**Date:** 2026-04-28
|
| 3 |
+
**Source:** C:\Users\DELL\Desktop\RAG_backup2
|
| 4 |
+
**Destination:** D:\RAG_Backup_2026_04_28
|
| 5 |
+
|
| 6 |
+
## Core Settings (from app/core/config.py)
|
| 7 |
+
- **App Name:** Fast RAG Chatbot
|
| 8 |
+
- **LLM Provider:** groq (default)
|
| 9 |
+
- **Groq Model:** llama-3.1-8b-instant
|
| 10 |
+
- **HF Model:** meta-llama/Llama-3.1-8B-Instruct
|
| 11 |
+
- **Embedding Model:** BAAI/bge-small-en-v1.5
|
| 12 |
+
- **Docs Directory:** docs/
|
| 13 |
+
- **Index Directory:** data/index/
|
| 14 |
+
- **Sessions Directory:** data/sessions/
|
| 15 |
+
- **Chunk Size:** 420 tokens
|
| 16 |
+
- **Overlap:** 80 tokens
|
| 17 |
+
- **Top K:** 4
|
| 18 |
+
|
| 19 |
+
## Admin Credentials
|
| 20 |
+
- **Username:** martech_admin
|
| 21 |
+
- **Password:** martech_admin_303
|
| 22 |
+
- **OTP Expiry:** 300 seconds (5 minutes)
|
| 23 |
+
|
| 24 |
+
## Security
|
| 25 |
+
- **Auth Method:** HTTP Basic Auth + OTP (email-based)
|
| 26 |
+
- **Email for OTP:** randomjoedown@gmail.com
|
| 27 |
+
- **SMTP Server:** smtp.gmail.com (Port 465)
|
| 28 |
+
|
| 29 |
+
## API / Integration
|
| 30 |
+
- **Endpoint:** /api/chat
|
| 31 |
+
- **Widget Endpoint:** /widget
|
| 32 |
+
- **Admin Endpoint:** /admin
|
| 33 |
+
- **CORS:** Allowed for all (*) for testing
|
| 34 |
+
|
| 35 |
+
## Infrastructure
|
| 36 |
+
- **Hugging Face Path:** /data (for persistent storage)
|
| 37 |
+
- **Local Path:** data/ (for local storage)
|
| 38 |
+
- **Dockerfile:** Based on python:3.10-slim, serves on port 7860
|