Martechsol commited on
Commit
d6ca65c
·
1 Parent(s): e33f2d7

Final sync: Ensure all intelligence and formatting fixes are pushed

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. backup_2026-05-04/.env.example +15 -0
  2. backup_2026-05-04/.gitattributes +2 -0
  3. backup_2026-05-04/.gitignore +9 -0
  4. backup_2026-05-04/CONFIG_NOTES.md +38 -0
  5. backup_2026-05-04/CUSTOMIZATION_GUIDE.md +55 -0
  6. backup_2026-05-04/Dockerfile +31 -0
  7. backup_2026-05-04/PHPMailer/Exception.php +40 -0
  8. backup_2026-05-04/PHPMailer/PHPMailer.php +0 -0
  9. backup_2026-05-04/PHPMailer/SMTP.php +1509 -0
  10. backup_2026-05-04/PROJECT_OVERVIEW.md +32 -0
  11. backup_2026-05-04/README.md +164 -0
  12. backup_2026-05-04/app.py +6 -0
  13. backup_2026-05-04/app/__init__.py +0 -0
  14. backup_2026-05-04/app/admin/__init__.py +1 -0
  15. backup_2026-05-04/app/admin/router.py +346 -0
  16. backup_2026-05-04/app/admin/templates/admin.html +1143 -0
  17. backup_2026-05-04/app/api/__init__.py +0 -0
  18. backup_2026-05-04/app/api/schemas.py +27 -0
  19. backup_2026-05-04/app/core/__init__.py +0 -0
  20. backup_2026-05-04/app/core/config.py +51 -0
  21. backup_2026-05-04/app/main.py +196 -0
  22. backup_2026-05-04/app/services/__init__.py +0 -0
  23. backup_2026-05-04/app/services/chunker.py +39 -0
  24. backup_2026-05-04/app/services/document_loader.py +37 -0
  25. backup_2026-05-04/app/services/embeddings.py +34 -0
  26. backup_2026-05-04/app/services/llm.py +264 -0
  27. backup_2026-05-04/app/services/rag_pipeline.py +98 -0
  28. backup_2026-05-04/app/services/rate_limiter.py +24 -0
  29. backup_2026-05-04/app/services/reranker.py +26 -0
  30. backup_2026-05-04/app/services/session_store.py +284 -0
  31. backup_2026-05-04/app/services/vector_store.py +167 -0
  32. backup_2026-05-04/app/ui_gradio.py +275 -0
  33. backup_2026-05-04/docker-compose.yml +14 -0
  34. backup_2026-05-04/docs/MartechSol_Employee_HandBook (7) (1).pdf +3 -0
  35. backup_2026-05-04/docs/MartechSol_Employee_Handbook_Extracted.txt +1091 -0
  36. backup_2026-05-04/floaitng-icon/KhloeSAC.lottie +3 -0
  37. backup_2026-05-04/get_models.py +25 -0
  38. backup_2026-05-04/martech_sol_logo.jpg +0 -0
  39. backup_2026-05-04/requirements.txt +14 -0
  40. backup_2026-05-04/static/addon.html +543 -0
  41. backup_2026-05-04/static/bot-icon.lottie +3 -0
  42. backup_2026-05-04/static/chat-loader.js +48 -0
  43. backup_2026-05-04/test_groq.py +26 -0
  44. backup_2026-05-04/wordpress code/WP_INTEGRATION_SCRIPT.txt +12 -0
  45. backup_2026-05-04/wordpress code/addon.html +477 -0
  46. backup_2026-05-04/wordpress code/addon_backup.html +175 -0
  47. backup_2026-05-13_pre_intelligent_retrieval/.env.example +19 -0
  48. backup_2026-05-13_pre_intelligent_retrieval/.gitattributes +2 -0
  49. backup_2026-05-13_pre_intelligent_retrieval/.gitignore +9 -0
  50. 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
1084
+ .replace(/"/g, '&quot;');
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