Andrew McCracken
commited on
Commit
·
2fb680d
1
Parent(s):
7ba4b03
Initial deployment to Spaces
Browse files- .dockerignore +22 -0
- Dockerfile +37 -0
- README.md +29 -5
- knowledge_base.py +1069 -0
- llm_handler.py +185 -0
- main.py +480 -0
- monitoring.py +302 -0
- optimisations.py +253 -0
- requirements.txt +108 -0
- test_interface.html +334 -0
.dockerignore
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__
|
| 2 |
+
*.pyc
|
| 3 |
+
*.pyo
|
| 4 |
+
*.pyd
|
| 5 |
+
.Python
|
| 6 |
+
*.so
|
| 7 |
+
*.egg
|
| 8 |
+
*.egg-info
|
| 9 |
+
dist
|
| 10 |
+
build
|
| 11 |
+
.env
|
| 12 |
+
.venv
|
| 13 |
+
venv
|
| 14 |
+
env
|
| 15 |
+
.git
|
| 16 |
+
.gitignore
|
| 17 |
+
.idea
|
| 18 |
+
.vscode
|
| 19 |
+
*.db
|
| 20 |
+
knowledge_db
|
| 21 |
+
models
|
| 22 |
+
*.log
|
Dockerfile
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.12-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# Install system dependencies
|
| 6 |
+
RUN apt-get update && apt-get install -y \
|
| 7 |
+
gcc \
|
| 8 |
+
g++ \
|
| 9 |
+
make \
|
| 10 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 11 |
+
|
| 12 |
+
# Copy requirements and install
|
| 13 |
+
COPY requirements.txt .
|
| 14 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 15 |
+
|
| 16 |
+
# Copy application code
|
| 17 |
+
COPY . .
|
| 18 |
+
|
| 19 |
+
# Create data directory for persistence
|
| 20 |
+
RUN mkdir -p /data
|
| 21 |
+
|
| 22 |
+
# Set environment variables
|
| 23 |
+
ENV PYTHONUNBUFFERED=1
|
| 24 |
+
ENV MODEL_REPO=daskalos-apps/phi4-cybersec-Q4_K_M
|
| 25 |
+
ENV MODEL_FILENAME=phi4-mini-instruct-Q4_K_M.gguf
|
| 26 |
+
ENV USE_RAG=true
|
| 27 |
+
ENV CACHE_ENABLED=true
|
| 28 |
+
|
| 29 |
+
# Expose port
|
| 30 |
+
EXPOSE 8000
|
| 31 |
+
|
| 32 |
+
# Health check
|
| 33 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
| 34 |
+
CMD python -c "import requests; requests.get('http://localhost:8000/health')"
|
| 35 |
+
|
| 36 |
+
# Run the application
|
| 37 |
+
CMD ["python", "main.py"]
|
README.md
CHANGED
|
@@ -1,11 +1,35 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
license: mit
|
|
|
|
| 9 |
---
|
| 10 |
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Cybersecurity Training Chatbot
|
| 3 |
+
emoji: 🔒
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
license: mit
|
| 9 |
+
app_port: 8000
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# Cybersecurity Training Chatbot
|
| 13 |
+
|
| 14 |
+
An AI-powered cybersecurity training assistant using Microsoft's Phi-4 model.
|
| 15 |
+
|
| 16 |
+
## Features
|
| 17 |
+
|
| 18 |
+
- 🤖 AI-powered security guidance
|
| 19 |
+
- 💬 Real-time chat interface
|
| 20 |
+
- 📊 Interaction tracking
|
| 21 |
+
- 🔍 RAG-enhanced responses
|
| 22 |
+
- ⚡ Fast CPU inference
|
| 23 |
+
|
| 24 |
+
## Usage
|
| 25 |
+
|
| 26 |
+
Simply start chatting with the bot about cybersecurity topics!
|
| 27 |
+
|
| 28 |
+
Access the test interface at: `/test`
|
| 29 |
+
|
| 30 |
+
## API Endpoints
|
| 31 |
+
|
| 32 |
+
- `/test` - Chat interface
|
| 33 |
+
- `/health` - Health check
|
| 34 |
+
- `/docs` - API documentation
|
| 35 |
+
- `/interactions/count` - View total interactions
|
knowledge_base.py
ADDED
|
@@ -0,0 +1,1069 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
from typing import List, Dict, Any, Optional, Generator
|
| 4 |
+
from dataclasses import dataclass
|
| 5 |
+
from enum import Enum
|
| 6 |
+
import hashlib
|
| 7 |
+
import logging
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
|
| 10 |
+
import chromadb
|
| 11 |
+
from chromadb.config import Settings
|
| 12 |
+
from sentence_transformers import SentenceTransformer
|
| 13 |
+
from huggingface_hub import hf_hub_download
|
| 14 |
+
|
| 15 |
+
# Import the base LLM handler
|
| 16 |
+
from llm_handler import CybersecurityLLM
|
| 17 |
+
|
| 18 |
+
logging.basicConfig(level=logging.INFO)
|
| 19 |
+
logger = logging.getLogger(__name__)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
# ================================================
|
| 23 |
+
# Security Knowledge Definitions
|
| 24 |
+
# ================================================
|
| 25 |
+
|
| 26 |
+
class SecurityTopic(Enum):
|
| 27 |
+
PHISHING = "phishing"
|
| 28 |
+
PASSWORDS = "passwords"
|
| 29 |
+
MALWARE = "malware"
|
| 30 |
+
SOCIAL_ENGINEERING = "social_engineering"
|
| 31 |
+
DATA_PROTECTION = "data_protection"
|
| 32 |
+
NETWORK_SECURITY = "network_security"
|
| 33 |
+
INCIDENT_RESPONSE = "incident_response"
|
| 34 |
+
PHYSICAL_SECURITY = "physical_security"
|
| 35 |
+
MOBILE_SECURITY = "mobile_security"
|
| 36 |
+
CLOUD_SECURITY = "cloud_security"
|
| 37 |
+
COMPLIANCE = "compliance"
|
| 38 |
+
EMAIL_SECURITY = "email_security"
|
| 39 |
+
RANSOMWARE = "ransomware"
|
| 40 |
+
ZERO_TRUST = "zero_trust"
|
| 41 |
+
SUPPLY_CHAIN = "supply_chain"
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
@dataclass
|
| 45 |
+
class SecurityKnowledge:
|
| 46 |
+
topic: SecurityTopic
|
| 47 |
+
title: str
|
| 48 |
+
content: str
|
| 49 |
+
keywords: List[str]
|
| 50 |
+
severity: str # low, medium, high, critical
|
| 51 |
+
last_updated: str = ""
|
| 52 |
+
|
| 53 |
+
def to_dict(self) -> Dict[str, Any]:
|
| 54 |
+
return {
|
| 55 |
+
"topic": self.topic.value,
|
| 56 |
+
"title": self.title,
|
| 57 |
+
"content": self.content,
|
| 58 |
+
"keywords": json.dumps(self.keywords), # Serialize list to JSON string
|
| 59 |
+
"severity": self.severity,
|
| 60 |
+
"last_updated": self.last_updated or datetime.now().isoformat()
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
# ================================================
|
| 65 |
+
# Main Knowledge Base Class
|
| 66 |
+
# ================================================
|
| 67 |
+
|
| 68 |
+
class CybersecurityKnowledgeBase:
|
| 69 |
+
def __init__(self,
|
| 70 |
+
persist_directory: str = "./knowledge_db",
|
| 71 |
+
embedding_model: str = "all-MiniLM-L6-v2"):
|
| 72 |
+
"""
|
| 73 |
+
Initialize knowledge base with vector database
|
| 74 |
+
|
| 75 |
+
Args:
|
| 76 |
+
persist_directory: Directory to persist ChromaDB
|
| 77 |
+
embedding_model: Sentence transformer model for embeddings
|
| 78 |
+
"""
|
| 79 |
+
|
| 80 |
+
logger.info(f"Initializing knowledge base at {persist_directory}")
|
| 81 |
+
|
| 82 |
+
# Create directory if it doesn't exist
|
| 83 |
+
os.makedirs(persist_directory, exist_ok=True)
|
| 84 |
+
|
| 85 |
+
# Initialize ChromaDB with persistence
|
| 86 |
+
self.client = chromadb.PersistentClient(
|
| 87 |
+
path=persist_directory,
|
| 88 |
+
settings=Settings(
|
| 89 |
+
anonymized_telemetry=False,
|
| 90 |
+
allow_reset=True
|
| 91 |
+
)
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
# Create or get collection
|
| 95 |
+
try:
|
| 96 |
+
self.collection = self.client.get_collection("cybersecurity_knowledge")
|
| 97 |
+
logger.info(f"Loaded existing collection with {self.collection.count()} documents")
|
| 98 |
+
except:
|
| 99 |
+
self.collection = self.client.create_collection(
|
| 100 |
+
name="cybersecurity_knowledge",
|
| 101 |
+
metadata={"description": "Cybersecurity best practices and knowledge"}
|
| 102 |
+
)
|
| 103 |
+
logger.info("Created new knowledge collection")
|
| 104 |
+
|
| 105 |
+
# Initialize embedder
|
| 106 |
+
logger.info(f"Loading embedding model: {embedding_model}")
|
| 107 |
+
self.embedder = SentenceTransformer(embedding_model)
|
| 108 |
+
|
| 109 |
+
# Load core knowledge if collection is empty
|
| 110 |
+
if self.collection.count() == 0:
|
| 111 |
+
logger.info("Loading core cybersecurity knowledge...")
|
| 112 |
+
self._load_core_knowledge()
|
| 113 |
+
|
| 114 |
+
# Track statistics
|
| 115 |
+
self.stats = {
|
| 116 |
+
"total_documents": self.collection.count(),
|
| 117 |
+
"queries_processed": 0,
|
| 118 |
+
"last_updated": datetime.now().isoformat()
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
def _load_core_knowledge(self):
|
| 122 |
+
"""Load comprehensive cybersecurity knowledge"""
|
| 123 |
+
|
| 124 |
+
knowledge_items = [
|
| 125 |
+
# Phishing and Email Security
|
| 126 |
+
SecurityKnowledge(
|
| 127 |
+
topic=SecurityTopic.PHISHING,
|
| 128 |
+
title="Comprehensive Phishing Detection Guide",
|
| 129 |
+
content="""
|
| 130 |
+
IDENTIFYING PHISHING EMAILS - Complete Guide:
|
| 131 |
+
|
| 132 |
+
Red Flags to Watch For:
|
| 133 |
+
• Generic greetings: "Dear Customer" instead of your actual name
|
| 134 |
+
• Urgency tactics: "Act now or your account will be closed!"
|
| 135 |
+
• Grammar/spelling errors: Professional companies proofread their emails
|
| 136 |
+
• Mismatched sender: Display name doesn't match email address
|
| 137 |
+
• Suspicious links: Hover to see if URL matches claimed sender
|
| 138 |
+
• Unexpected attachments: Especially .zip, .exe, .scr, .vbs files
|
| 139 |
+
• Requests for sensitive info: Legitimate companies don't ask for passwords via email
|
| 140 |
+
• Too good to be true: "You've won $1 million!"
|
| 141 |
+
• Emotional manipulation: Fear, greed, curiosity, sympathy
|
| 142 |
+
|
| 143 |
+
How to Verify Suspicious Emails:
|
| 144 |
+
1. Check sender's email address carefully (not just display name)
|
| 145 |
+
2. Hover over links WITHOUT clicking to preview destination
|
| 146 |
+
3. Look for HTTPS and correct domain in links
|
| 147 |
+
4. Contact company directly through official channels (not email links)
|
| 148 |
+
5. Check for personalization - legitimate emails often include account numbers
|
| 149 |
+
6. Verify with IT security team when in doubt
|
| 150 |
+
|
| 151 |
+
What to Do If You Receive Phishing:
|
| 152 |
+
1. Don't click links or download attachments
|
| 153 |
+
2. Don't reply or provide any information
|
| 154 |
+
3. Report to IT security immediately
|
| 155 |
+
4. Forward to anti-phishing team if available
|
| 156 |
+
5. Delete the email after reporting
|
| 157 |
+
6. Warn colleagues if it's widespread
|
| 158 |
+
|
| 159 |
+
If You Clicked a Phishing Link:
|
| 160 |
+
1. Disconnect from network immediately
|
| 161 |
+
2. Change passwords from a different device
|
| 162 |
+
3. Report to IT security IMMEDIATELY
|
| 163 |
+
4. Run antivirus scan
|
| 164 |
+
5. Monitor accounts for suspicious activity
|
| 165 |
+
6. Enable MFA on all accounts if not already done
|
| 166 |
+
""",
|
| 167 |
+
keywords=["phishing", "email", "scam", "suspicious", "link", "attachment", "spear phishing", "whaling",
|
| 168 |
+
"BEC"],
|
| 169 |
+
severity="critical"
|
| 170 |
+
),
|
| 171 |
+
|
| 172 |
+
# Password Security
|
| 173 |
+
SecurityKnowledge(
|
| 174 |
+
topic=SecurityTopic.PASSWORDS,
|
| 175 |
+
title="Password Security Best Practices",
|
| 176 |
+
content="""
|
| 177 |
+
CREATING STRONG PASSWORDS:
|
| 178 |
+
|
| 179 |
+
Requirements for Strong Passwords:
|
| 180 |
+
• Minimum 12-16 characters (longer is better)
|
| 181 |
+
• Mix of uppercase and lowercase letters
|
| 182 |
+
• Include numbers and special characters (!@#$%^&*)
|
| 183 |
+
• Avoid dictionary words and personal information
|
| 184 |
+
• Unique for every account - never reuse passwords
|
| 185 |
+
• Consider passphrases: 'Coffee@7Makes$Me!Happy2024'
|
| 186 |
+
• Avoid patterns: Password1, Password2, etc.
|
| 187 |
+
• Don't use keyboard patterns: qwerty, 123456
|
| 188 |
+
|
| 189 |
+
Password Management Best Practices:
|
| 190 |
+
• Use a reputable password manager (Bitwarden, 1Password, LastPass)
|
| 191 |
+
• Enable two-factor authentication (2FA) everywhere possible
|
| 192 |
+
• Use authenticator apps over SMS when possible
|
| 193 |
+
• Never share passwords via email, chat, or phone
|
| 194 |
+
• Change passwords immediately if breach suspected
|
| 195 |
+
• Don't write passwords on sticky notes
|
| 196 |
+
• Use different passwords for work and personal accounts
|
| 197 |
+
• Consider using hardware keys for critical accounts
|
| 198 |
+
|
| 199 |
+
Multi-Factor Authentication (MFA):
|
| 200 |
+
• Something you know (password)
|
| 201 |
+
• Something you have (phone, token)
|
| 202 |
+
• Something you are (biometric)
|
| 203 |
+
|
| 204 |
+
Password Manager Benefits:
|
| 205 |
+
• Generate random, unique passwords
|
| 206 |
+
• Securely store all passwords
|
| 207 |
+
• Auto-fill credentials safely
|
| 208 |
+
• Sync across devices
|
| 209 |
+
• Alert you to breaches
|
| 210 |
+
• Share passwords securely when needed
|
| 211 |
+
|
| 212 |
+
Common Password Mistakes:
|
| 213 |
+
• Using personal information (birthdate, pet names)
|
| 214 |
+
• Reusing passwords across sites
|
| 215 |
+
• Sharing passwords with others
|
| 216 |
+
• Using simple substitutions (P@ssw0rd)
|
| 217 |
+
• Not updating default passwords
|
| 218 |
+
• Ignoring breach notifications
|
| 219 |
+
""",
|
| 220 |
+
keywords=["password", "authentication", "2FA", "MFA", "login", "credentials", "passphrase",
|
| 221 |
+
"password manager"],
|
| 222 |
+
severity="critical"
|
| 223 |
+
),
|
| 224 |
+
|
| 225 |
+
# Malware Prevention
|
| 226 |
+
SecurityKnowledge(
|
| 227 |
+
topic=SecurityTopic.MALWARE,
|
| 228 |
+
title="Malware Prevention and Response",
|
| 229 |
+
content="""
|
| 230 |
+
MALWARE PREVENTION STRATEGIES:
|
| 231 |
+
|
| 232 |
+
Prevention Best Practices:
|
| 233 |
+
• Keep OS and all software updated with latest patches
|
| 234 |
+
• Use reputable antivirus with real-time protection
|
| 235 |
+
• Enable Windows Defender or equivalent
|
| 236 |
+
• Download software only from official sources
|
| 237 |
+
• Verify digital signatures on downloads
|
| 238 |
+
• Scan USB drives before opening files
|
| 239 |
+
• Disable macros in Office documents from unknown sources
|
| 240 |
+
• Use application sandboxing when possible
|
| 241 |
+
• Regular backups following 3-2-1 rule
|
| 242 |
+
• Keep UAC (User Account Control) enabled
|
| 243 |
+
|
| 244 |
+
Types of Malware:
|
| 245 |
+
• Viruses: Self-replicating, attaches to files
|
| 246 |
+
• Worms: Self-spreading through networks
|
| 247 |
+
• Trojans: Disguised as legitimate software
|
| 248 |
+
• Ransomware: Encrypts files for ransom
|
| 249 |
+
• Spyware: Steals information secretly
|
| 250 |
+
• Adware: Displays unwanted advertisements
|
| 251 |
+
• Rootkits: Hides presence from system
|
| 252 |
+
• Keyloggers: Records keystrokes
|
| 253 |
+
• Cryptominers: Uses resources to mine cryptocurrency
|
| 254 |
+
|
| 255 |
+
Warning Signs of Infection:
|
| 256 |
+
• Computer running unusually slow
|
| 257 |
+
• Frequent crashes or blue screens
|
| 258 |
+
• Programs starting automatically
|
| 259 |
+
• Browser homepage changed
|
| 260 |
+
• New toolbars or extensions
|
| 261 |
+
• Excessive pop-ups
|
| 262 |
+
• Files encrypted with ransom note
|
| 263 |
+
• Unusual network activity
|
| 264 |
+
• Disabled security software
|
| 265 |
+
• Missing or modified files
|
| 266 |
+
|
| 267 |
+
If Infected - Immediate Steps:
|
| 268 |
+
1. Disconnect from all networks (WiFi, Ethernet)
|
| 269 |
+
2. Enter Safe Mode if possible
|
| 270 |
+
3. Run full antivirus scan
|
| 271 |
+
4. Use additional malware removal tools (Malwarebytes)
|
| 272 |
+
5. Check for system restore points
|
| 273 |
+
6. Contact IT security team immediately
|
| 274 |
+
7. Change all passwords from clean device
|
| 275 |
+
8. Monitor financial accounts
|
| 276 |
+
9. Consider complete system reinstall for severe infections
|
| 277 |
+
""",
|
| 278 |
+
keywords=["malware", "virus", "ransomware", "trojan", "antivirus", "infection", "worm", "spyware"],
|
| 279 |
+
severity="critical"
|
| 280 |
+
),
|
| 281 |
+
|
| 282 |
+
# Social Engineering
|
| 283 |
+
SecurityKnowledge(
|
| 284 |
+
topic=SecurityTopic.SOCIAL_ENGINEERING,
|
| 285 |
+
title="Social Engineering Defense Strategies",
|
| 286 |
+
content="""
|
| 287 |
+
DEFENDING AGAINST SOCIAL ENGINEERING:
|
| 288 |
+
|
| 289 |
+
Common Social Engineering Tactics:
|
| 290 |
+
• Pretexting: Creating fake scenarios to steal information
|
| 291 |
+
• Baiting: Offering something enticing (USB drives, downloads)
|
| 292 |
+
• Quid pro quo: Offering service for information
|
| 293 |
+
• Tailgating: Following into secure areas
|
| 294 |
+
• Vishing: Voice phishing via phone
|
| 295 |
+
• Smishing: SMS/text message phishing
|
| 296 |
+
• Watering hole: Compromising frequently visited websites
|
| 297 |
+
• Dumpster diving: Searching trash for information
|
| 298 |
+
• Shoulder surfing: Looking over shoulder for passwords
|
| 299 |
+
|
| 300 |
+
Red Flags to Recognize:
|
| 301 |
+
• Unsolicited contact asking for information
|
| 302 |
+
• Urgency without verification
|
| 303 |
+
• Requests to bypass normal procedures
|
| 304 |
+
• Appeals to authority without proof
|
| 305 |
+
• Offers that seem too good to be true
|
| 306 |
+
• Requests for passwords or sensitive data
|
| 307 |
+
• Emotional manipulation (fear, greed, sympathy)
|
| 308 |
+
• Name dropping without context
|
| 309 |
+
• Resistance to verification
|
| 310 |
+
|
| 311 |
+
Defense Strategies:
|
| 312 |
+
• Always verify identity before sharing information
|
| 313 |
+
• Use callback numbers from official sources
|
| 314 |
+
• Be suspicious of unsolicited contacts
|
| 315 |
+
• Never give passwords over phone/email
|
| 316 |
+
• Question unusual requests, even from "colleagues"
|
| 317 |
+
• Report suspicious behavior immediately
|
| 318 |
+
• Trust but verify - confirm through separate channel
|
| 319 |
+
• Be aware of information you share publicly
|
| 320 |
+
• Secure physical documents and screens
|
| 321 |
+
• Educate family about work-related scams
|
| 322 |
+
|
| 323 |
+
Verification Techniques:
|
| 324 |
+
• Call back on known number
|
| 325 |
+
• Check employee directory
|
| 326 |
+
• Verify with manager
|
| 327 |
+
• Ask for employee ID
|
| 328 |
+
• Request email confirmation
|
| 329 |
+
• Check digital signatures
|
| 330 |
+
• Verify through IT security
|
| 331 |
+
""",
|
| 332 |
+
keywords=["social engineering", "pretexting", "vishing", "smishing", "manipulation", "tailgating",
|
| 333 |
+
"phishing"],
|
| 334 |
+
severity="high"
|
| 335 |
+
),
|
| 336 |
+
|
| 337 |
+
# Network Security
|
| 338 |
+
SecurityKnowledge(
|
| 339 |
+
topic=SecurityTopic.NETWORK_SECURITY,
|
| 340 |
+
title="Network and WiFi Security Guide",
|
| 341 |
+
content="""
|
| 342 |
+
NETWORK SECURITY BEST PRACTICES:
|
| 343 |
+
|
| 344 |
+
Home WiFi Security:
|
| 345 |
+
• Change default router admin credentials immediately
|
| 346 |
+
• Use WPA3 encryption (WPA2 minimum)
|
| 347 |
+
• Create strong WiFi password (20+ characters)
|
| 348 |
+
• Change default network name (SSID)
|
| 349 |
+
• Disable WPS (WiFi Protected Setup)
|
| 350 |
+
• Keep router firmware updated monthly
|
| 351 |
+
• Use guest network for visitors and IoT devices
|
| 352 |
+
• Disable remote management unless necessary
|
| 353 |
+
• Turn off SSID broadcast if practical
|
| 354 |
+
• Use MAC address filtering for added security
|
| 355 |
+
• Position router centrally to minimize external signal
|
| 356 |
+
• Regular reboot router (monthly)
|
| 357 |
+
|
| 358 |
+
Public WiFi Safety:
|
| 359 |
+
• Avoid accessing sensitive accounts
|
| 360 |
+
• Always use VPN for all connections
|
| 361 |
+
• Verify network name with venue staff
|
| 362 |
+
• Turn off automatic WiFi connection
|
| 363 |
+
• Forget network after use
|
| 364 |
+
• Never accept certificate warnings
|
| 365 |
+
• Disable file sharing
|
| 366 |
+
• Use cellular data for sensitive tasks
|
| 367 |
+
• Keep firewall enabled
|
| 368 |
+
• Use HTTPS websites only
|
| 369 |
+
|
| 370 |
+
VPN Best Practices:
|
| 371 |
+
• Use company-approved VPN only
|
| 372 |
+
• Connect before accessing any resources
|
| 373 |
+
• Keep VPN client updated
|
| 374 |
+
• Report connection issues immediately
|
| 375 |
+
• Don't use free/public VPN services
|
| 376 |
+
• Verify VPN is active before working
|
| 377 |
+
|
| 378 |
+
Network Hygiene:
|
| 379 |
+
• Regular network scans for unknown devices
|
| 380 |
+
• Monitor bandwidth usage
|
| 381 |
+
• Check for unauthorized access points
|
| 382 |
+
• Secure all network equipment physically
|
| 383 |
+
• Document network configuration
|
| 384 |
+
• Regular security audits
|
| 385 |
+
""",
|
| 386 |
+
keywords=["wifi", "network", "router", "VPN", "encryption", "WPA3", "public wifi", "wireless"],
|
| 387 |
+
severity="high"
|
| 388 |
+
),
|
| 389 |
+
|
| 390 |
+
# Incident Response
|
| 391 |
+
SecurityKnowledge(
|
| 392 |
+
topic=SecurityTopic.INCIDENT_RESPONSE,
|
| 393 |
+
title="Security Incident Response Procedures",
|
| 394 |
+
content="""
|
| 395 |
+
SECURITY INCIDENT RESPONSE GUIDE:
|
| 396 |
+
|
| 397 |
+
IMMEDIATE RESPONSE STEPS:
|
| 398 |
+
1. STOP - Don't try to fix it yourself
|
| 399 |
+
2. DISCONNECT - Unplug network cable or disable WiFi
|
| 400 |
+
3. DOCUMENT - Write down:
|
| 401 |
+
- What happened
|
| 402 |
+
- When it occurred
|
| 403 |
+
- What you were doing
|
| 404 |
+
- Error messages
|
| 405 |
+
- Unusual behavior observed
|
| 406 |
+
4. REPORT - Contact IT security immediately
|
| 407 |
+
5. PRESERVE - Don't delete anything, take screenshots
|
| 408 |
+
6. WAIT - For IT security instructions
|
| 409 |
+
|
| 410 |
+
Types of Incidents Requiring Immediate Reporting:
|
| 411 |
+
• Clicked suspicious link or attachment
|
| 412 |
+
• Entered credentials on suspicious site
|
| 413 |
+
• Lost device with company data
|
| 414 |
+
• Suspicious computer behavior
|
| 415 |
+
• Unauthorized access attempts
|
| 416 |
+
• Data breach or leak discovered
|
| 417 |
+
• Ransomware infection
|
| 418 |
+
• Physical security breach
|
| 419 |
+
• Stolen credentials
|
| 420 |
+
• Suspicious phone calls asking for info
|
| 421 |
+
|
| 422 |
+
Information to Provide:
|
| 423 |
+
• Your name and contact information
|
| 424 |
+
• Time and date of incident
|
| 425 |
+
• Affected systems/accounts
|
| 426 |
+
• Description of what happened
|
| 427 |
+
• Actions taken so far
|
| 428 |
+
• Any error messages (exact wording)
|
| 429 |
+
• Screenshots if possible
|
| 430 |
+
• Anyone else who might be affected
|
| 431 |
+
|
| 432 |
+
DO NOT:
|
| 433 |
+
• Try to fix it yourself
|
| 434 |
+
• Delete or modify evidence
|
| 435 |
+
• Inform unauthorized people
|
| 436 |
+
• Post about it on social media
|
| 437 |
+
• Continue using affected systems
|
| 438 |
+
• Pay ransoms
|
| 439 |
+
|
| 440 |
+
Contact Information:
|
| 441 |
+
IT Security Hotline: [Organization specific]
|
| 442 |
+
Email: security@[organization]
|
| 443 |
+
After hours: [Emergency contact]
|
| 444 |
+
""",
|
| 445 |
+
keywords=["incident", "breach", "response", "report", "emergency", "compromise", "security incident"],
|
| 446 |
+
severity="critical"
|
| 447 |
+
),
|
| 448 |
+
|
| 449 |
+
# Data Protection
|
| 450 |
+
SecurityKnowledge(
|
| 451 |
+
topic=SecurityTopic.DATA_PROTECTION,
|
| 452 |
+
title="Data Protection and Privacy Guide",
|
| 453 |
+
content="""
|
| 454 |
+
DATA PROTECTION BEST PRACTICES:
|
| 455 |
+
|
| 456 |
+
Data Classification:
|
| 457 |
+
• Public: Can be freely shared
|
| 458 |
+
• Internal: Within organization only
|
| 459 |
+
��� Confidential: Specific authorized individuals
|
| 460 |
+
• Restricted: Highest sensitivity, strict controls
|
| 461 |
+
|
| 462 |
+
Handling Sensitive Data:
|
| 463 |
+
• Encrypt files before sharing externally
|
| 464 |
+
• Use approved file sharing platforms only
|
| 465 |
+
• Never use personal email for work data
|
| 466 |
+
• Implement clean desk policy
|
| 467 |
+
• Lock computer when stepping away (Win+L or Cmd+Ctrl+Q)
|
| 468 |
+
• Use privacy screens in public spaces
|
| 469 |
+
• Shred physical documents with sensitive info
|
| 470 |
+
• Secure disposal of electronic media
|
| 471 |
+
• Don't discuss sensitive info in public
|
| 472 |
+
• Be aware of smart speakers/devices
|
| 473 |
+
|
| 474 |
+
Encryption Best Practices:
|
| 475 |
+
• Use full disk encryption (BitLocker, FileVault)
|
| 476 |
+
• Encrypt removable media
|
| 477 |
+
• Use encrypted communication channels
|
| 478 |
+
• Encrypt email with sensitive data
|
| 479 |
+
• Password protect sensitive documents
|
| 480 |
+
• Use enterprise encryption tools
|
| 481 |
+
• Store encryption keys securely
|
| 482 |
+
|
| 483 |
+
Data Backup Practices:
|
| 484 |
+
• Follow 3-2-1 rule:
|
| 485 |
+
- 3 copies of important data
|
| 486 |
+
- 2 different storage media
|
| 487 |
+
- 1 offsite backup
|
| 488 |
+
• Test restore procedures regularly
|
| 489 |
+
• Encrypt backup drives
|
| 490 |
+
• Store backups securely
|
| 491 |
+
• Automate where possible
|
| 492 |
+
• Document what's backed up
|
| 493 |
+
• Verify backup integrity
|
| 494 |
+
|
| 495 |
+
Privacy Considerations:
|
| 496 |
+
• Minimize data collection
|
| 497 |
+
• Only share need-to-know basis
|
| 498 |
+
• Regular data audits
|
| 499 |
+
• Respect retention policies
|
| 500 |
+
• Secure data destruction
|
| 501 |
+
• GDPR/CCPA compliance
|
| 502 |
+
""",
|
| 503 |
+
keywords=["data", "encryption", "backup", "confidential", "sensitive", "GDPR", "privacy",
|
| 504 |
+
"classification"],
|
| 505 |
+
severity="high"
|
| 506 |
+
),
|
| 507 |
+
|
| 508 |
+
# Mobile Security
|
| 509 |
+
SecurityKnowledge(
|
| 510 |
+
topic=SecurityTopic.MOBILE_SECURITY,
|
| 511 |
+
title="Mobile Device Security Guidelines",
|
| 512 |
+
content="""
|
| 513 |
+
MOBILE DEVICE SECURITY:
|
| 514 |
+
|
| 515 |
+
Device Security Settings:
|
| 516 |
+
• Enable screen lock (PIN, password, biometric)
|
| 517 |
+
• Set auto-lock to 1-2 minutes
|
| 518 |
+
• Keep OS and apps updated automatically
|
| 519 |
+
• Download apps only from official stores
|
| 520 |
+
• Review app permissions carefully
|
| 521 |
+
• Enable remote wipe capability
|
| 522 |
+
• Use Find My Device features
|
| 523 |
+
• Encrypt device storage
|
| 524 |
+
• Disable Bluetooth when not needed
|
| 525 |
+
• Turn off WiFi auto-connect
|
| 526 |
+
• Disable Siri/Assistant on lock screen
|
| 527 |
+
|
| 528 |
+
BYOD (Bring Your Own Device) Security:
|
| 529 |
+
• Separate work and personal data
|
| 530 |
+
• Use MDM if required by company
|
| 531 |
+
• Install company security apps
|
| 532 |
+
• Follow company mobile policy
|
| 533 |
+
• Report lost/stolen immediately
|
| 534 |
+
• Don't jailbreak/root devices
|
| 535 |
+
• Use company VPN for work
|
| 536 |
+
• Regular security updates
|
| 537 |
+
|
| 538 |
+
Mobile Threats:
|
| 539 |
+
• Malicious apps
|
| 540 |
+
• Unsecured WiFi
|
| 541 |
+
• SMiShing (SMS phishing)
|
| 542 |
+
• Bluetooth attacks
|
| 543 |
+
• Physical theft
|
| 544 |
+
• Shoulder surfing
|
| 545 |
+
• Juice jacking (USB charging)
|
| 546 |
+
• SIM swapping
|
| 547 |
+
|
| 548 |
+
Safe Mobile Practices:
|
| 549 |
+
• Avoid public WiFi for sensitive tasks
|
| 550 |
+
• Use VPN when on public networks
|
| 551 |
+
• Don't click links in text messages
|
| 552 |
+
• Be cautious with QR codes
|
| 553 |
+
• Use official app stores only
|
| 554 |
+
• Keep personal info private
|
| 555 |
+
• Regular app permission audits
|
| 556 |
+
• Backup device regularly
|
| 557 |
+
• Use mobile antivirus
|
| 558 |
+
• Avoid charging at public USB ports
|
| 559 |
+
""",
|
| 560 |
+
keywords=["mobile", "smartphone", "tablet", "BYOD", "iOS", "Android", "app security", "MDM"],
|
| 561 |
+
severity="medium"
|
| 562 |
+
),
|
| 563 |
+
|
| 564 |
+
# Ransomware Specific
|
| 565 |
+
SecurityKnowledge(
|
| 566 |
+
topic=SecurityTopic.RANSOMWARE,
|
| 567 |
+
title="Ransomware Prevention and Response",
|
| 568 |
+
content="""
|
| 569 |
+
RANSOMWARE PROTECTION GUIDE:
|
| 570 |
+
|
| 571 |
+
Prevention Strategies:
|
| 572 |
+
• Regular automated backups (tested restores)
|
| 573 |
+
• Keep all software patched and updated
|
| 574 |
+
• Email filtering and sandboxing
|
| 575 |
+
• Disable macros by default
|
| 576 |
+
• User training on phishing
|
| 577 |
+
• Network segmentation
|
| 578 |
+
• Principle of least privilege
|
| 579 |
+
• Application whitelisting
|
| 580 |
+
• Endpoint detection and response (EDR)
|
| 581 |
+
|
| 582 |
+
If Ransomware Strikes:
|
| 583 |
+
1. Immediately disconnect from network
|
| 584 |
+
2. Power off if actively encrypting
|
| 585 |
+
3. Report to IT security immediately
|
| 586 |
+
4. Do NOT pay ransom
|
| 587 |
+
5. Preserve evidence for investigation
|
| 588 |
+
6. Check for decryption tools
|
| 589 |
+
7. Restore from clean backups
|
| 590 |
+
8. Rebuild affected systems
|
| 591 |
+
9. Investigate root cause
|
| 592 |
+
10. Implement lessons learned
|
| 593 |
+
|
| 594 |
+
Warning Signs:
|
| 595 |
+
• Files with strange extensions
|
| 596 |
+
• Cannot open documents
|
| 597 |
+
• Ransom notes in folders
|
| 598 |
+
• Slow computer performance
|
| 599 |
+
• Renamed files
|
| 600 |
+
• Wallpaper changed to ransom message
|
| 601 |
+
|
| 602 |
+
Recovery Planning:
|
| 603 |
+
• Maintain offline backups
|
| 604 |
+
• Test restore procedures
|
| 605 |
+
• Document critical systems
|
| 606 |
+
• Incident response plan
|
| 607 |
+
• Communication plan
|
| 608 |
+
• Legal/law enforcement contacts
|
| 609 |
+
""",
|
| 610 |
+
keywords=["ransomware", "encryption", "ransom", "backup", "recovery", "bitcoin", "crypto"],
|
| 611 |
+
severity="critical"
|
| 612 |
+
),
|
| 613 |
+
|
| 614 |
+
# Cloud Security
|
| 615 |
+
SecurityKnowledge(
|
| 616 |
+
topic=SecurityTopic.CLOUD_SECURITY,
|
| 617 |
+
title="Cloud Services Security",
|
| 618 |
+
content="""
|
| 619 |
+
CLOUD SECURITY BEST PRACTICES:
|
| 620 |
+
|
| 621 |
+
Account Security:
|
| 622 |
+
• Use strong, unique passwords
|
| 623 |
+
• Enable MFA on all cloud accounts
|
| 624 |
+
• Regular access reviews
|
| 625 |
+
• Monitor for unusual activity
|
| 626 |
+
• Use SSO where available
|
| 627 |
+
• Secure API keys and tokens
|
| 628 |
+
|
| 629 |
+
Data Protection in Cloud:
|
| 630 |
+
• Understand shared responsibility model
|
| 631 |
+
• Encrypt data at rest and in transit
|
| 632 |
+
• Use cloud provider encryption
|
| 633 |
+
• Control data residency
|
| 634 |
+
• Regular security audits
|
| 635 |
+
• Implement DLP policies
|
| 636 |
+
|
| 637 |
+
Safe Cloud Usage:
|
| 638 |
+
• Only use approved cloud services
|
| 639 |
+
• Read terms of service
|
| 640 |
+
• Understand data ownership
|
| 641 |
+
• Configure privacy settings
|
| 642 |
+
• Regular permission reviews
|
| 643 |
+
• Monitor shared links
|
| 644 |
+
• Set expiration on shares
|
| 645 |
+
• Audit access logs
|
| 646 |
+
|
| 647 |
+
Common Cloud Risks:
|
| 648 |
+
• Misconfigured storage buckets
|
| 649 |
+
• Excessive permissions
|
| 650 |
+
• Shadow IT
|
| 651 |
+
• Account takeover
|
| 652 |
+
• Data leakage
|
| 653 |
+
• Compliance violations
|
| 654 |
+
• Insider threats
|
| 655 |
+
• API vulnerabilities
|
| 656 |
+
""",
|
| 657 |
+
keywords=["cloud", "SaaS", "AWS", "Azure", "Google Cloud", "OneDrive", "Dropbox", "Office 365"],
|
| 658 |
+
severity="high"
|
| 659 |
+
)
|
| 660 |
+
]
|
| 661 |
+
|
| 662 |
+
# Add all knowledge items to vector database
|
| 663 |
+
batch_size = 10
|
| 664 |
+
for i in range(0, len(knowledge_items), batch_size):
|
| 665 |
+
batch = knowledge_items[i:i + batch_size]
|
| 666 |
+
|
| 667 |
+
embeddings = []
|
| 668 |
+
documents = []
|
| 669 |
+
metadatas = []
|
| 670 |
+
ids = []
|
| 671 |
+
|
| 672 |
+
for item in batch:
|
| 673 |
+
# Generate embedding
|
| 674 |
+
embedding = self.embedder.encode(item.content).tolist()
|
| 675 |
+
embeddings.append(embedding)
|
| 676 |
+
|
| 677 |
+
# Prepare document
|
| 678 |
+
documents.append(item.content)
|
| 679 |
+
|
| 680 |
+
# Prepare metadata
|
| 681 |
+
metadatas.append(item.to_dict())
|
| 682 |
+
|
| 683 |
+
# Generate unique ID
|
| 684 |
+
doc_id = hashlib.md5(
|
| 685 |
+
f"{item.topic.value}_{item.title}_{len(item.content)}".encode()
|
| 686 |
+
).hexdigest()
|
| 687 |
+
ids.append(doc_id)
|
| 688 |
+
|
| 689 |
+
# Add batch to collection
|
| 690 |
+
self.collection.add(
|
| 691 |
+
embeddings=embeddings,
|
| 692 |
+
documents=documents,
|
| 693 |
+
metadatas=metadatas,
|
| 694 |
+
ids=ids
|
| 695 |
+
)
|
| 696 |
+
|
| 697 |
+
logger.info(f"Added batch {i // batch_size + 1} of knowledge items")
|
| 698 |
+
|
| 699 |
+
logger.info(f"Successfully loaded {len(knowledge_items)} knowledge items")
|
| 700 |
+
|
| 701 |
+
def search(self,
|
| 702 |
+
query: str,
|
| 703 |
+
k: int = 3,
|
| 704 |
+
filter_topic: Optional[str] = None,
|
| 705 |
+
min_severity: Optional[str] = None) -> List[Dict[str, Any]]:
|
| 706 |
+
"""
|
| 707 |
+
Search for relevant security information
|
| 708 |
+
|
| 709 |
+
Args:
|
| 710 |
+
query: User's question
|
| 711 |
+
k: Number of results to return
|
| 712 |
+
filter_topic: Optional topic filter
|
| 713 |
+
min_severity: Minimum severity level filter
|
| 714 |
+
|
| 715 |
+
Returns:
|
| 716 |
+
List of relevant documents with metadata
|
| 717 |
+
"""
|
| 718 |
+
|
| 719 |
+
self.stats["queries_processed"] += 1
|
| 720 |
+
|
| 721 |
+
# Generate query embedding
|
| 722 |
+
query_embedding = self.embedder.encode(query).tolist()
|
| 723 |
+
|
| 724 |
+
# Build filter
|
| 725 |
+
where_filter = {}
|
| 726 |
+
if filter_topic:
|
| 727 |
+
where_filter["topic"] = filter_topic
|
| 728 |
+
if min_severity:
|
| 729 |
+
severity_levels = ["low", "medium", "high", "critical"]
|
| 730 |
+
min_index = severity_levels.index(min_severity)
|
| 731 |
+
valid_severities = severity_levels[min_index:]
|
| 732 |
+
where_filter["severity"] = {"$in": valid_severities}
|
| 733 |
+
|
| 734 |
+
# Search with or without filter
|
| 735 |
+
if where_filter:
|
| 736 |
+
results = self.collection.query(
|
| 737 |
+
query_embeddings=[query_embedding],
|
| 738 |
+
n_results=k,
|
| 739 |
+
where=where_filter
|
| 740 |
+
)
|
| 741 |
+
else:
|
| 742 |
+
results = self.collection.query(
|
| 743 |
+
query_embeddings=[query_embedding],
|
| 744 |
+
n_results=k
|
| 745 |
+
)
|
| 746 |
+
|
| 747 |
+
# Format results
|
| 748 |
+
formatted_results = []
|
| 749 |
+
if results['documents'] and results['documents'][0]:
|
| 750 |
+
for doc, metadata, distance in zip(
|
| 751 |
+
results['documents'][0],
|
| 752 |
+
results['metadatas'][0],
|
| 753 |
+
results['distances'][0]
|
| 754 |
+
):
|
| 755 |
+
formatted_results.append({
|
| 756 |
+
'content': doc,
|
| 757 |
+
'topic': metadata.get('topic', 'unknown'),
|
| 758 |
+
'title': metadata.get('title', 'Untitled'),
|
| 759 |
+
'severity': metadata.get('severity', 'medium'),
|
| 760 |
+
'keywords': json.loads(metadata.get('keywords', '[]')), # Deserialize JSON string back to list
|
| 761 |
+
'relevance_score': 1 - (distance / 2) # Convert distance to similarity
|
| 762 |
+
})
|
| 763 |
+
|
| 764 |
+
return formatted_results
|
| 765 |
+
|
| 766 |
+
def add_custom_knowledge(self,
|
| 767 |
+
content: str,
|
| 768 |
+
topic: str,
|
| 769 |
+
title: str,
|
| 770 |
+
keywords: List[str],
|
| 771 |
+
severity: str = "medium") -> bool:
|
| 772 |
+
"""
|
| 773 |
+
Add custom security knowledge to the database
|
| 774 |
+
|
| 775 |
+
Args:
|
| 776 |
+
content: Knowledge content
|
| 777 |
+
topic: Topic category
|
| 778 |
+
title: Title of the knowledge
|
| 779 |
+
keywords: Related keywords
|
| 780 |
+
severity: Severity level
|
| 781 |
+
|
| 782 |
+
Returns:
|
| 783 |
+
Success status
|
| 784 |
+
"""
|
| 785 |
+
|
| 786 |
+
try:
|
| 787 |
+
# Generate embedding
|
| 788 |
+
embedding = self.embedder.encode(content).tolist()
|
| 789 |
+
|
| 790 |
+
# Generate unique ID
|
| 791 |
+
doc_id = hashlib.md5(
|
| 792 |
+
f"{topic}_{title}_{len(content)}_{datetime.now().isoformat()}".encode()
|
| 793 |
+
).hexdigest()
|
| 794 |
+
|
| 795 |
+
# Add to collection
|
| 796 |
+
self.collection.add(
|
| 797 |
+
embeddings=[embedding],
|
| 798 |
+
documents=[content],
|
| 799 |
+
metadatas=[{
|
| 800 |
+
"topic": topic,
|
| 801 |
+
"title": title,
|
| 802 |
+
"keywords": json.dumps(keywords), # Serialize list to JSON string
|
| 803 |
+
"severity": severity,
|
| 804 |
+
"last_updated": datetime.now().isoformat(),
|
| 805 |
+
"custom": True
|
| 806 |
+
}],
|
| 807 |
+
ids=[doc_id]
|
| 808 |
+
)
|
| 809 |
+
|
| 810 |
+
self.stats["total_documents"] = self.collection.count()
|
| 811 |
+
logger.info(f"Added custom knowledge: {title}")
|
| 812 |
+
return True
|
| 813 |
+
|
| 814 |
+
except Exception as e:
|
| 815 |
+
logger.error(f"Failed to add custom knowledge: {e}")
|
| 816 |
+
return False
|
| 817 |
+
|
| 818 |
+
def get_statistics(self) -> Dict[str, Any]:
|
| 819 |
+
"""Get knowledge base statistics"""
|
| 820 |
+
|
| 821 |
+
self.stats["total_documents"] = self.collection.count()
|
| 822 |
+
self.stats["last_accessed"] = datetime.now().isoformat()
|
| 823 |
+
|
| 824 |
+
# Get topic distribution
|
| 825 |
+
all_metadata = self.collection.get()['metadatas']
|
| 826 |
+
topic_counts = {}
|
| 827 |
+
severity_counts = {}
|
| 828 |
+
|
| 829 |
+
for metadata in all_metadata:
|
| 830 |
+
topic = metadata.get('topic', 'unknown')
|
| 831 |
+
severity = metadata.get('severity', 'unknown')
|
| 832 |
+
|
| 833 |
+
topic_counts[topic] = topic_counts.get(topic, 0) + 1
|
| 834 |
+
severity_counts[severity] = severity_counts.get(severity, 0) + 1
|
| 835 |
+
|
| 836 |
+
self.stats["topic_distribution"] = topic_counts
|
| 837 |
+
self.stats["severity_distribution"] = severity_counts
|
| 838 |
+
|
| 839 |
+
return self.stats
|
| 840 |
+
|
| 841 |
+
def export_knowledge(self, output_file: str = "knowledge_export.json") -> bool:
|
| 842 |
+
"""Export all knowledge to JSON file"""
|
| 843 |
+
|
| 844 |
+
try:
|
| 845 |
+
all_data = self.collection.get()
|
| 846 |
+
|
| 847 |
+
export_data = {
|
| 848 |
+
"exported_at": datetime.now().isoformat(),
|
| 849 |
+
"total_documents": len(all_data['ids']),
|
| 850 |
+
"documents": []
|
| 851 |
+
}
|
| 852 |
+
|
| 853 |
+
for doc, metadata, doc_id in zip(
|
| 854 |
+
all_data['documents'],
|
| 855 |
+
all_data['metadatas'],
|
| 856 |
+
all_data['ids']
|
| 857 |
+
):
|
| 858 |
+
export_data["documents"].append({
|
| 859 |
+
"id": doc_id,
|
| 860 |
+
"content": doc,
|
| 861 |
+
"metadata": metadata
|
| 862 |
+
})
|
| 863 |
+
|
| 864 |
+
with open(output_file, 'w') as f:
|
| 865 |
+
json.dump(export_data, f, indent=2)
|
| 866 |
+
|
| 867 |
+
logger.info(f"Exported knowledge to {output_file}")
|
| 868 |
+
return True
|
| 869 |
+
|
| 870 |
+
except Exception as e:
|
| 871 |
+
logger.error(f"Failed to export knowledge: {e}")
|
| 872 |
+
return False
|
| 873 |
+
|
| 874 |
+
|
| 875 |
+
# ================================================
|
| 876 |
+
# RAG-Enhanced LLM Class
|
| 877 |
+
# ================================================
|
| 878 |
+
|
| 879 |
+
class RAGCybersecurityLLM(CybersecurityLLM):
|
| 880 |
+
def __init__(self,
|
| 881 |
+
repo_id: str = "daskalos-apps/phi4-cybersec-Q4_K_M",
|
| 882 |
+
filename: str = "phi4-mini-instruct-Q4_K_M.gguf",
|
| 883 |
+
local_dir: str = "./models",
|
| 884 |
+
knowledge_dir: str = "./knowledge_db",
|
| 885 |
+
force_download: bool = False):
|
| 886 |
+
"""
|
| 887 |
+
Initialize LLM with RAG capabilities
|
| 888 |
+
|
| 889 |
+
Args:
|
| 890 |
+
repo_id: Hugging Face repository ID
|
| 891 |
+
filename: Model filename
|
| 892 |
+
local_dir: Local cache directory
|
| 893 |
+
knowledge_dir: Knowledge base directory
|
| 894 |
+
force_download: Force model re-download
|
| 895 |
+
"""
|
| 896 |
+
|
| 897 |
+
# Initialize base LLM
|
| 898 |
+
super().__init__(repo_id, filename, local_dir, force_download)
|
| 899 |
+
|
| 900 |
+
# Initialize knowledge base
|
| 901 |
+
logger.info("Initializing RAG knowledge base...")
|
| 902 |
+
self.knowledge_base = CybersecurityKnowledgeBase(persist_directory=knowledge_dir)
|
| 903 |
+
|
| 904 |
+
# Enhanced system prompt for RAG
|
| 905 |
+
self.rag_prompt_template = """<|system|>
|
| 906 |
+
{system}
|
| 907 |
+
|
| 908 |
+
You have access to a comprehensive cybersecurity knowledge base. Use the provided context to give accurate, detailed answers. If the context doesn't contain relevant information, use your general knowledge but indicate when you're doing so.
|
| 909 |
+
<|end|>
|
| 910 |
+
<|user|>
|
| 911 |
+
Context from knowledge base:
|
| 912 |
+
{context}
|
| 913 |
+
|
| 914 |
+
User Question: {user}
|
| 915 |
+
<|end|>
|
| 916 |
+
<|assistant|>"""
|
| 917 |
+
|
| 918 |
+
def generate_with_rag(self,
|
| 919 |
+
prompt: str,
|
| 920 |
+
max_tokens: int = 512,
|
| 921 |
+
use_rag: bool = True,
|
| 922 |
+
k_documents: int = 3,
|
| 923 |
+
min_relevance: float = 0.5) -> Dict[str, Any]:
|
| 924 |
+
"""
|
| 925 |
+
Generate response with RAG enhancement
|
| 926 |
+
|
| 927 |
+
Args:
|
| 928 |
+
prompt: User's question
|
| 929 |
+
max_tokens: Maximum response length
|
| 930 |
+
use_rag: Whether to use RAG
|
| 931 |
+
k_documents: Number of documents to retrieve
|
| 932 |
+
min_relevance: Minimum relevance threshold
|
| 933 |
+
|
| 934 |
+
Returns:
|
| 935 |
+
Response with metadata and sources
|
| 936 |
+
"""
|
| 937 |
+
|
| 938 |
+
context = None
|
| 939 |
+
sources = []
|
| 940 |
+
|
| 941 |
+
if use_rag:
|
| 942 |
+
# Search knowledge base
|
| 943 |
+
logger.info(f"Searching knowledge base for: {prompt[:50]}...")
|
| 944 |
+
relevant_docs = self.knowledge_base.search(prompt, k=k_documents)
|
| 945 |
+
|
| 946 |
+
# Filter by relevance
|
| 947 |
+
relevant_docs = [
|
| 948 |
+
doc for doc in relevant_docs
|
| 949 |
+
if doc.get('relevance_score', 0) >= min_relevance
|
| 950 |
+
]
|
| 951 |
+
|
| 952 |
+
if relevant_docs:
|
| 953 |
+
# Build context from relevant documents
|
| 954 |
+
context_parts = []
|
| 955 |
+
for i, doc in enumerate(relevant_docs, 1):
|
| 956 |
+
context_parts.append(
|
| 957 |
+
f"[Source {i}: {doc['title']} - Severity: {doc['severity']}]\n"
|
| 958 |
+
f"{doc['content'][:1000]}..." # Limit context length
|
| 959 |
+
)
|
| 960 |
+
sources.append({
|
| 961 |
+
"title": doc['title'],
|
| 962 |
+
"topic": doc['topic'],
|
| 963 |
+
"severity": doc['severity'],
|
| 964 |
+
"relevance": doc['relevance_score']
|
| 965 |
+
})
|
| 966 |
+
|
| 967 |
+
context = "\n\n".join(context_parts)
|
| 968 |
+
logger.info(f"Found {len(relevant_docs)} relevant documents")
|
| 969 |
+
else:
|
| 970 |
+
logger.info("No highly relevant documents found")
|
| 971 |
+
|
| 972 |
+
# Generate response
|
| 973 |
+
if context and use_rag:
|
| 974 |
+
# Use RAG prompt template
|
| 975 |
+
full_prompt = self.rag_prompt_template.format(
|
| 976 |
+
system=self.system_prompt,
|
| 977 |
+
context=context,
|
| 978 |
+
user=prompt
|
| 979 |
+
)
|
| 980 |
+
else:
|
| 981 |
+
# Use standard prompt template
|
| 982 |
+
full_prompt = self.format_prompt(prompt)
|
| 983 |
+
|
| 984 |
+
try:
|
| 985 |
+
response = self.llm(
|
| 986 |
+
full_prompt,
|
| 987 |
+
max_tokens=max_tokens,
|
| 988 |
+
temperature=0.7,
|
| 989 |
+
top_p=0.95,
|
| 990 |
+
top_k=40,
|
| 991 |
+
repeat_penalty=1.1,
|
| 992 |
+
stop=self.stop_tokens,
|
| 993 |
+
echo=False
|
| 994 |
+
)
|
| 995 |
+
|
| 996 |
+
text = response['choices'][0]['text'].strip()
|
| 997 |
+
|
| 998 |
+
return {
|
| 999 |
+
"response": text,
|
| 1000 |
+
"tokens_used": response['usage']['total_tokens'],
|
| 1001 |
+
"model": self.model_info['repo_id'],
|
| 1002 |
+
"sources": sources,
|
| 1003 |
+
"rag_used": use_rag and bool(context)
|
| 1004 |
+
}
|
| 1005 |
+
|
| 1006 |
+
except Exception as e:
|
| 1007 |
+
logger.error(f"Generation error: {e}")
|
| 1008 |
+
return {
|
| 1009 |
+
"response": "I apologize, but I encountered an error. Please try rephrasing your question.",
|
| 1010 |
+
"error": str(e),
|
| 1011 |
+
"sources": [],
|
| 1012 |
+
"rag_used": False
|
| 1013 |
+
}
|
| 1014 |
+
|
| 1015 |
+
def generate_stream_with_rag(self,
|
| 1016 |
+
prompt: str,
|
| 1017 |
+
max_tokens: int = 512,
|
| 1018 |
+
use_rag: bool = True,
|
| 1019 |
+
k_documents: int = 3) -> Generator:
|
| 1020 |
+
"""Stream response with RAG enhancement"""
|
| 1021 |
+
|
| 1022 |
+
# Get context if using RAG
|
| 1023 |
+
context = None
|
| 1024 |
+
if use_rag:
|
| 1025 |
+
relevant_docs = self.knowledge_base.search(prompt, k=k_documents)
|
| 1026 |
+
if relevant_docs:
|
| 1027 |
+
context_parts = [f"{doc['title']}: {doc['content'][:500]}" for doc in relevant_docs]
|
| 1028 |
+
context = "\n\n".join(context_parts)
|
| 1029 |
+
|
| 1030 |
+
# Generate prompt
|
| 1031 |
+
if context:
|
| 1032 |
+
full_prompt = self.rag_prompt_template.format(
|
| 1033 |
+
system=self.system_prompt,
|
| 1034 |
+
context=context,
|
| 1035 |
+
user=prompt
|
| 1036 |
+
)
|
| 1037 |
+
else:
|
| 1038 |
+
full_prompt = self.format_prompt(prompt)
|
| 1039 |
+
|
| 1040 |
+
# Stream response
|
| 1041 |
+
stream = self.llm(
|
| 1042 |
+
full_prompt,
|
| 1043 |
+
max_tokens=max_tokens,
|
| 1044 |
+
temperature=0.7,
|
| 1045 |
+
top_p=0.95,
|
| 1046 |
+
top_k=40,
|
| 1047 |
+
repeat_penalty=1.1,
|
| 1048 |
+
stop=self.stop_tokens,
|
| 1049 |
+
echo=False,
|
| 1050 |
+
stream=True
|
| 1051 |
+
)
|
| 1052 |
+
|
| 1053 |
+
for output in stream:
|
| 1054 |
+
token = output['choices'][0].get('text', '')
|
| 1055 |
+
if token:
|
| 1056 |
+
yield token
|
| 1057 |
+
|
| 1058 |
+
def add_knowledge(self, content: str, topic: str, title: str, keywords: List[str]) -> bool:
|
| 1059 |
+
"""Add new knowledge to the RAG system"""
|
| 1060 |
+
return self.knowledge_base.add_custom_knowledge(
|
| 1061 |
+
content=content,
|
| 1062 |
+
topic=topic,
|
| 1063 |
+
title=title,
|
| 1064 |
+
keywords=keywords
|
| 1065 |
+
)
|
| 1066 |
+
|
| 1067 |
+
def get_knowledge_stats(self) -> Dict[str, Any]:
|
| 1068 |
+
"""Get knowledge base statistics"""
|
| 1069 |
+
return self.knowledge_base.get_statistics()
|
llm_handler.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from llama_cpp import Llama
|
| 2 |
+
from typing import Generator, Optional, Dict, Any
|
| 3 |
+
import logging
|
| 4 |
+
import os
|
| 5 |
+
from huggingface_hub import hf_hub_download
|
| 6 |
+
import hashlib
|
| 7 |
+
|
| 8 |
+
logging.basicConfig(level=logging.INFO)
|
| 9 |
+
logger = logging.getLogger(__name__)
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class CybersecurityLLM:
|
| 13 |
+
def __init__(self,
|
| 14 |
+
repo_id: str = "daskalos-apps/phi4-cybersec-Q4_K_M",
|
| 15 |
+
filename: str = "phi4-mini-instruct-Q4_K_M.gguf",
|
| 16 |
+
local_dir: str = "./models",
|
| 17 |
+
force_download: bool = False):
|
| 18 |
+
"""
|
| 19 |
+
Initialize Phi-4 from Hugging Face
|
| 20 |
+
|
| 21 |
+
Args:
|
| 22 |
+
repo_id: Your Hugging Face repository ID
|
| 23 |
+
filename: The GGUF filename in the repository
|
| 24 |
+
local_dir: Local directory to cache the model
|
| 25 |
+
force_download: Force re-download even if cached
|
| 26 |
+
"""
|
| 27 |
+
|
| 28 |
+
# Create local directory if it doesn't exist
|
| 29 |
+
os.makedirs(local_dir, exist_ok=True)
|
| 30 |
+
|
| 31 |
+
# Download model from Hugging Face
|
| 32 |
+
logger.info(f"Loading model from Hugging Face: {repo_id}")
|
| 33 |
+
|
| 34 |
+
try:
|
| 35 |
+
model_path = hf_hub_download(
|
| 36 |
+
repo_id=repo_id,
|
| 37 |
+
filename=filename,
|
| 38 |
+
local_dir=local_dir,
|
| 39 |
+
local_dir_use_symlinks=False,
|
| 40 |
+
force_download=force_download
|
| 41 |
+
)
|
| 42 |
+
logger.info(f"Model downloaded/cached at: {model_path}")
|
| 43 |
+
except Exception as e:
|
| 44 |
+
logger.error(f"Failed to download model: {e}")
|
| 45 |
+
# Fallback to local file if exists
|
| 46 |
+
model_path = os.path.join(local_dir, filename)
|
| 47 |
+
if not os.path.exists(model_path):
|
| 48 |
+
raise FileNotFoundError(f"Model not found locally or on Hugging Face: {repo_id}")
|
| 49 |
+
|
| 50 |
+
# Initialize llama.cpp with the model
|
| 51 |
+
logger.info("Initializing model...")
|
| 52 |
+
self.llm = Llama(
|
| 53 |
+
model_path=model_path,
|
| 54 |
+
n_ctx=4096, # Context window
|
| 55 |
+
n_batch=512, # Batch size for prompt processing
|
| 56 |
+
n_threads=8, # Adjust based on CPU cores
|
| 57 |
+
n_gpu_layers=0, # CPU only
|
| 58 |
+
seed=-1, # Random seed
|
| 59 |
+
f16_kv=True, # Use f16 for key/value cache
|
| 60 |
+
logits_all=False, # Only compute logits for last token
|
| 61 |
+
vocab_only=False, # Load full model
|
| 62 |
+
use_mmap=True, # Memory-map model for efficiency
|
| 63 |
+
use_mlock=False, # Don't lock model in RAM
|
| 64 |
+
verbose=False
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
# Store model info
|
| 68 |
+
self.model_info = {
|
| 69 |
+
"repo_id": repo_id,
|
| 70 |
+
"filename": filename,
|
| 71 |
+
"path": model_path,
|
| 72 |
+
"size_mb": os.path.getsize(model_path) / (1024 * 1024)
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
# Cybersecurity-focused system prompt
|
| 76 |
+
self.system_prompt = """You are a cybersecurity expert assistant helping employees understand and implement security best practices. Your role is to provide clear, actionable guidance that non-technical users can understand and apply.
|
| 77 |
+
|
| 78 |
+
Core expertise areas:
|
| 79 |
+
• Email Security & Phishing Detection
|
| 80 |
+
• Password Management & Authentication
|
| 81 |
+
• Malware Prevention & Detection
|
| 82 |
+
• Safe Browsing & Download Practices
|
| 83 |
+
• Data Protection & Encryption
|
| 84 |
+
• Social Engineering Defense
|
| 85 |
+
• Remote Work Security
|
| 86 |
+
• Incident Response & Reporting
|
| 87 |
+
• Physical Security
|
| 88 |
+
• Mobile Device Security
|
| 89 |
+
• Cloud Security Basics
|
| 90 |
+
• Compliance Basics (GDPR, HIPAA, etc.)
|
| 91 |
+
|
| 92 |
+
Guidelines:
|
| 93 |
+
- Always prioritize user safety and security
|
| 94 |
+
- Provide step-by-step instructions when applicable
|
| 95 |
+
- Use simple language, avoid excessive jargon
|
| 96 |
+
- Include real-world examples
|
| 97 |
+
- Emphasize prevention over remediation
|
| 98 |
+
- Never ask users to disable security features
|
| 99 |
+
- If unsure, recommend consulting IT security team"""
|
| 100 |
+
|
| 101 |
+
# Phi-4 uses ChatML format
|
| 102 |
+
self.prompt_template = """<|system|>
|
| 103 |
+
{system}<|end|>
|
| 104 |
+
<|user|>
|
| 105 |
+
{user}<|end|>
|
| 106 |
+
<|assistant|>"""
|
| 107 |
+
|
| 108 |
+
self.stop_tokens = ["<|end|>", "<|user|>", "<|endoftext|>", "<|assistant|>"]
|
| 109 |
+
|
| 110 |
+
logger.info(f"Model ready! Size: {self.model_info['size_mb']:.2f} MB")
|
| 111 |
+
|
| 112 |
+
def format_prompt(self, user_input: str, context: Optional[str] = None) -> str:
|
| 113 |
+
"""Format prompt with optional context for RAG"""
|
| 114 |
+
if context:
|
| 115 |
+
user_input = f"Context: {context}\n\nQuestion: {user_input}"
|
| 116 |
+
|
| 117 |
+
return self.prompt_template.format(
|
| 118 |
+
system=self.system_prompt,
|
| 119 |
+
user=user_input
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
def generate(self,
|
| 123 |
+
prompt: str,
|
| 124 |
+
max_tokens: int = 512,
|
| 125 |
+
temperature: float = 0.7,
|
| 126 |
+
context: Optional[str] = None) -> Dict[str, Any]:
|
| 127 |
+
"""Generate response with metadata"""
|
| 128 |
+
|
| 129 |
+
full_prompt = self.format_prompt(prompt, context)
|
| 130 |
+
|
| 131 |
+
try:
|
| 132 |
+
response = self.llm(
|
| 133 |
+
full_prompt,
|
| 134 |
+
max_tokens=max_tokens,
|
| 135 |
+
temperature=temperature,
|
| 136 |
+
top_p=0.95,
|
| 137 |
+
top_k=40,
|
| 138 |
+
repeat_penalty=1.1,
|
| 139 |
+
stop=self.stop_tokens,
|
| 140 |
+
echo=False
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
text = response['choices'][0]['text'].strip()
|
| 144 |
+
|
| 145 |
+
return {
|
| 146 |
+
"response": text,
|
| 147 |
+
"tokens_used": response['usage']['total_tokens'],
|
| 148 |
+
"model": self.model_info['repo_id']
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
except Exception as e:
|
| 152 |
+
logger.error(f"Generation error: {e}")
|
| 153 |
+
return {
|
| 154 |
+
"response": "I apologize, but I encountered an error. Please try rephrasing your question.",
|
| 155 |
+
"error": str(e)
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
def generate_stream(self,
|
| 159 |
+
prompt: str,
|
| 160 |
+
max_tokens: int = 512,
|
| 161 |
+
context: Optional[str] = None) -> Generator:
|
| 162 |
+
"""Stream response tokens"""
|
| 163 |
+
|
| 164 |
+
full_prompt = self.format_prompt(prompt, context)
|
| 165 |
+
|
| 166 |
+
stream = self.llm(
|
| 167 |
+
full_prompt,
|
| 168 |
+
max_tokens=max_tokens,
|
| 169 |
+
temperature=0.7,
|
| 170 |
+
top_p=0.95,
|
| 171 |
+
top_k=40,
|
| 172 |
+
repeat_penalty=1.1,
|
| 173 |
+
stop=self.stop_tokens,
|
| 174 |
+
echo=False,
|
| 175 |
+
stream=True
|
| 176 |
+
)
|
| 177 |
+
|
| 178 |
+
for output in stream:
|
| 179 |
+
token = output['choices'][0].get('text', '')
|
| 180 |
+
if token:
|
| 181 |
+
yield token
|
| 182 |
+
|
| 183 |
+
def get_model_info(self) -> Dict[str, Any]:
|
| 184 |
+
"""Get information about the loaded model"""
|
| 185 |
+
return self.model_info
|
main.py
ADDED
|
@@ -0,0 +1,480 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect, BackgroundTasks
|
| 2 |
+
from fastapi.responses import StreamingResponse, JSONResponse, FileResponse
|
| 3 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 4 |
+
from pydantic import BaseModel, Field
|
| 5 |
+
from typing import Optional, List, Dict, Any
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
import asyncio
|
| 8 |
+
import json
|
| 9 |
+
import uuid
|
| 10 |
+
import os
|
| 11 |
+
import sqlite3
|
| 12 |
+
from contextlib import asynccontextmanager
|
| 13 |
+
|
| 14 |
+
# Import our handlers
|
| 15 |
+
from llm_handler import CybersecurityLLM
|
| 16 |
+
from knowledge_base import RAGCybersecurityLLM
|
| 17 |
+
from optimisations import PerformanceOptimizer, MemoryManager
|
| 18 |
+
|
| 19 |
+
# Configuration from environment variables
|
| 20 |
+
MODEL_REPO = os.getenv("MODEL_REPO", "daskalos-apps/phi4-cybersec-Q4_K_M")
|
| 21 |
+
MODEL_FILENAME = os.getenv("MODEL_FILENAME", "phi4-mini-instruct-Q4_K_M.gguf")
|
| 22 |
+
USE_RAG = os.getenv("USE_RAG", "true").lower() == "true"
|
| 23 |
+
CACHE_ENABLED = os.getenv("CACHE_ENABLED", "true").lower() == "true"
|
| 24 |
+
|
| 25 |
+
# Global instances
|
| 26 |
+
llm_instance = None
|
| 27 |
+
optimizer = None
|
| 28 |
+
memory_manager = None
|
| 29 |
+
|
| 30 |
+
# Database setup
|
| 31 |
+
# Support multiple deployment platforms: /data (HF Spaces), /app/data (Render/Railway), or local
|
| 32 |
+
if os.path.exists("/data"):
|
| 33 |
+
DB_PATH = "/data/interactions.db"
|
| 34 |
+
elif os.path.exists("/app/data"):
|
| 35 |
+
DB_PATH = "/app/data/interactions.db"
|
| 36 |
+
else:
|
| 37 |
+
DB_PATH = "interactions.db"
|
| 38 |
+
|
| 39 |
+
def init_db():
|
| 40 |
+
"""Initialize SQLite database for interaction tracking"""
|
| 41 |
+
conn = sqlite3.connect(DB_PATH)
|
| 42 |
+
cursor = conn.cursor()
|
| 43 |
+
cursor.execute("""
|
| 44 |
+
CREATE TABLE IF NOT EXISTS interactions (
|
| 45 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 46 |
+
timestamp TEXT NOT NULL,
|
| 47 |
+
session_id TEXT,
|
| 48 |
+
message TEXT,
|
| 49 |
+
response_length INTEGER
|
| 50 |
+
)
|
| 51 |
+
""")
|
| 52 |
+
cursor.execute("""
|
| 53 |
+
CREATE TABLE IF NOT EXISTS interaction_count (
|
| 54 |
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
| 55 |
+
count INTEGER DEFAULT 0
|
| 56 |
+
)
|
| 57 |
+
""")
|
| 58 |
+
cursor.execute("INSERT OR IGNORE INTO interaction_count (id, count) VALUES (1, 0)")
|
| 59 |
+
conn.commit()
|
| 60 |
+
conn.close()
|
| 61 |
+
|
| 62 |
+
def increment_interaction():
|
| 63 |
+
"""Increment interaction count and return new count"""
|
| 64 |
+
conn = sqlite3.connect(DB_PATH)
|
| 65 |
+
cursor = conn.cursor()
|
| 66 |
+
cursor.execute("UPDATE interaction_count SET count = count + 1 WHERE id = 1")
|
| 67 |
+
cursor.execute("SELECT count FROM interaction_count WHERE id = 1")
|
| 68 |
+
count = cursor.fetchone()[0]
|
| 69 |
+
conn.commit()
|
| 70 |
+
conn.close()
|
| 71 |
+
return count
|
| 72 |
+
|
| 73 |
+
def get_interaction_count():
|
| 74 |
+
"""Get current interaction count"""
|
| 75 |
+
conn = sqlite3.connect(DB_PATH)
|
| 76 |
+
cursor = conn.cursor()
|
| 77 |
+
cursor.execute("SELECT count FROM interaction_count WHERE id = 1")
|
| 78 |
+
count = cursor.fetchone()[0]
|
| 79 |
+
conn.close()
|
| 80 |
+
return count
|
| 81 |
+
|
| 82 |
+
def log_interaction(session_id: str, message: str, response_length: int):
|
| 83 |
+
"""Log interaction details"""
|
| 84 |
+
conn = sqlite3.connect(DB_PATH)
|
| 85 |
+
cursor = conn.cursor()
|
| 86 |
+
cursor.execute(
|
| 87 |
+
"INSERT INTO interactions (timestamp, session_id, message, response_length) VALUES (?, ?, ?, ?)",
|
| 88 |
+
(datetime.now().isoformat(), session_id, message, response_length)
|
| 89 |
+
)
|
| 90 |
+
conn.commit()
|
| 91 |
+
conn.close()
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
@asynccontextmanager
|
| 95 |
+
async def lifespan(app: FastAPI):
|
| 96 |
+
"""Startup and shutdown events"""
|
| 97 |
+
global llm_instance, optimizer, memory_manager
|
| 98 |
+
|
| 99 |
+
# Startup
|
| 100 |
+
print(f"🚀 Loading model from Hugging Face: {MODEL_REPO}")
|
| 101 |
+
|
| 102 |
+
# Initialize database
|
| 103 |
+
init_db()
|
| 104 |
+
print("✅ Database initialized")
|
| 105 |
+
|
| 106 |
+
try:
|
| 107 |
+
if USE_RAG:
|
| 108 |
+
llm_instance = RAGCybersecurityLLM(
|
| 109 |
+
repo_id=MODEL_REPO,
|
| 110 |
+
filename=MODEL_FILENAME
|
| 111 |
+
)
|
| 112 |
+
else:
|
| 113 |
+
llm_instance = CybersecurityLLM(
|
| 114 |
+
repo_id=MODEL_REPO,
|
| 115 |
+
filename=MODEL_FILENAME
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
if CACHE_ENABLED:
|
| 119 |
+
optimizer = PerformanceOptimizer()
|
| 120 |
+
|
| 121 |
+
memory_manager = MemoryManager()
|
| 122 |
+
|
| 123 |
+
print("✅ Cybersecurity Chatbot ready!")
|
| 124 |
+
print(f"📦 Model: {MODEL_REPO}")
|
| 125 |
+
print(f"💾 Size: {llm_instance.get_model_info()['size_mb']:.2f} MB")
|
| 126 |
+
print(f"🔧 RAG: {'Enabled' if USE_RAG else 'Disabled'}")
|
| 127 |
+
print(f"⚡ Cache: {'Enabled' if CACHE_ENABLED else 'Disabled'}")
|
| 128 |
+
|
| 129 |
+
except Exception as e:
|
| 130 |
+
print(f"❌ Failed to load model: {e}")
|
| 131 |
+
raise
|
| 132 |
+
|
| 133 |
+
yield
|
| 134 |
+
|
| 135 |
+
# Shutdown
|
| 136 |
+
print("👋 Shutting down...")
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
# Initialize FastAPI with lifespan
|
| 140 |
+
app = FastAPI(
|
| 141 |
+
title="Cybersecurity Training Chatbot API",
|
| 142 |
+
description="AI-powered cybersecurity guidance using Phi-4 from Hugging Face",
|
| 143 |
+
version="2.0.0",
|
| 144 |
+
lifespan=lifespan
|
| 145 |
+
)
|
| 146 |
+
|
| 147 |
+
# CORS for web interface
|
| 148 |
+
app.add_middleware(
|
| 149 |
+
CORSMiddleware,
|
| 150 |
+
allow_origins=["*"],
|
| 151 |
+
allow_credentials=True,
|
| 152 |
+
allow_methods=["*"],
|
| 153 |
+
allow_headers=["*"],
|
| 154 |
+
)
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
# Request/Response models
|
| 158 |
+
class ChatRequest(BaseModel):
|
| 159 |
+
message: str = Field(..., description="User's security question")
|
| 160 |
+
session_id: Optional[str] = Field(None, description="Session ID for conversation continuity")
|
| 161 |
+
max_tokens: Optional[int] = Field(512, description="Maximum response length")
|
| 162 |
+
temperature: Optional[float] = Field(0.7, description="Response creativity (0-1)")
|
| 163 |
+
use_rag: Optional[bool] = Field(True, description="Use RAG for enhanced accuracy")
|
| 164 |
+
use_cache: Optional[bool] = Field(True, description="Use cached responses if available")
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
class ChatResponse(BaseModel):
|
| 168 |
+
response: str
|
| 169 |
+
session_id: str
|
| 170 |
+
timestamp: str
|
| 171 |
+
model: str
|
| 172 |
+
tokens_used: Optional[int] = None
|
| 173 |
+
cached: bool = False
|
| 174 |
+
sources: Optional[List[str]] = None
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
class ModelInfo(BaseModel):
|
| 178 |
+
repo_id: str
|
| 179 |
+
filename: str
|
| 180 |
+
size_mb: float
|
| 181 |
+
rag_enabled: bool
|
| 182 |
+
cache_enabled: bool
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
# Session management
|
| 186 |
+
sessions: Dict[str, List[Dict[str, Any]]] = {}
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
@app.get("/", response_model=Dict[str, str])
|
| 190 |
+
async def root():
|
| 191 |
+
"""API root endpoint"""
|
| 192 |
+
return {
|
| 193 |
+
"message": "Cybersecurity Training Chatbot API",
|
| 194 |
+
"model": MODEL_REPO,
|
| 195 |
+
"documentation": "/docs",
|
| 196 |
+
"health": "/health"
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
@app.get("/health")
|
| 201 |
+
async def health_check():
|
| 202 |
+
"""Check API and model health"""
|
| 203 |
+
if llm_instance is None:
|
| 204 |
+
raise HTTPException(status_code=503, detail="Model not loaded")
|
| 205 |
+
|
| 206 |
+
memory_status = memory_manager.check_memory() if memory_manager else {}
|
| 207 |
+
|
| 208 |
+
return {
|
| 209 |
+
"status": "healthy",
|
| 210 |
+
"model": MODEL_REPO,
|
| 211 |
+
"version": "2.0.0",
|
| 212 |
+
"memory": memory_status,
|
| 213 |
+
"cache_enabled": CACHE_ENABLED,
|
| 214 |
+
"rag_enabled": USE_RAG
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
@app.get("/model/info", response_model=ModelInfo)
|
| 219 |
+
async def model_info():
|
| 220 |
+
"""Get information about the loaded model"""
|
| 221 |
+
if llm_instance is None:
|
| 222 |
+
raise HTTPException(status_code=503, detail="Model not loaded")
|
| 223 |
+
|
| 224 |
+
info = llm_instance.get_model_info()
|
| 225 |
+
|
| 226 |
+
return ModelInfo(
|
| 227 |
+
repo_id=info['repo_id'],
|
| 228 |
+
filename=info['filename'],
|
| 229 |
+
size_mb=info['size_mb'],
|
| 230 |
+
rag_enabled=USE_RAG,
|
| 231 |
+
cache_enabled=CACHE_ENABLED
|
| 232 |
+
)
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
@app.post("/chat", response_model=ChatResponse)
|
| 236 |
+
async def chat(request: ChatRequest):
|
| 237 |
+
"""Main chat endpoint"""
|
| 238 |
+
if llm_instance is None:
|
| 239 |
+
raise HTTPException(status_code=503, detail="Model not loaded")
|
| 240 |
+
|
| 241 |
+
try:
|
| 242 |
+
# Generate or get session ID
|
| 243 |
+
session_id = request.session_id or str(uuid.uuid4())
|
| 244 |
+
|
| 245 |
+
# Initialize session if new
|
| 246 |
+
if session_id not in sessions:
|
| 247 |
+
sessions[session_id] = []
|
| 248 |
+
|
| 249 |
+
# Store user message
|
| 250 |
+
sessions[session_id].append({
|
| 251 |
+
"role": "user",
|
| 252 |
+
"content": request.message,
|
| 253 |
+
"timestamp": datetime.now().isoformat()
|
| 254 |
+
})
|
| 255 |
+
|
| 256 |
+
# Check cache if enabled
|
| 257 |
+
cached = False
|
| 258 |
+
response_text = None
|
| 259 |
+
sources = None
|
| 260 |
+
|
| 261 |
+
if CACHE_ENABLED and request.use_cache and optimizer:
|
| 262 |
+
cached_response = optimizer.get_cached_response(request.message)
|
| 263 |
+
if cached_response:
|
| 264 |
+
response_text = cached_response
|
| 265 |
+
cached = True
|
| 266 |
+
|
| 267 |
+
# Generate response if not cached
|
| 268 |
+
if response_text is None:
|
| 269 |
+
if USE_RAG and hasattr(llm_instance, 'generate_with_rag'):
|
| 270 |
+
result = llm_instance.generate_with_rag(
|
| 271 |
+
request.message,
|
| 272 |
+
max_tokens=request.max_tokens,
|
| 273 |
+
use_rag=request.use_rag
|
| 274 |
+
)
|
| 275 |
+
sources = result.get('sources', [])
|
| 276 |
+
else:
|
| 277 |
+
result = llm_instance.generate(
|
| 278 |
+
request.message,
|
| 279 |
+
max_tokens=request.max_tokens,
|
| 280 |
+
temperature=request.temperature
|
| 281 |
+
)
|
| 282 |
+
|
| 283 |
+
response_text = result["response"]
|
| 284 |
+
|
| 285 |
+
# Cache the response
|
| 286 |
+
if CACHE_ENABLED and optimizer and request.use_cache:
|
| 287 |
+
optimizer.cache_response(request.message, response_text)
|
| 288 |
+
|
| 289 |
+
# Store assistant response
|
| 290 |
+
sessions[session_id].append({
|
| 291 |
+
"role": "assistant",
|
| 292 |
+
"content": response_text,
|
| 293 |
+
"timestamp": datetime.now().isoformat()
|
| 294 |
+
})
|
| 295 |
+
|
| 296 |
+
# Limit session history
|
| 297 |
+
if len(sessions[session_id]) > 20:
|
| 298 |
+
sessions[session_id] = sessions[session_id][-20:]
|
| 299 |
+
|
| 300 |
+
# Check memory usage
|
| 301 |
+
if memory_manager:
|
| 302 |
+
memory_manager.optimize_if_needed()
|
| 303 |
+
|
| 304 |
+
return ChatResponse(
|
| 305 |
+
response=response_text,
|
| 306 |
+
session_id=session_id,
|
| 307 |
+
timestamp=datetime.now().isoformat(),
|
| 308 |
+
model=MODEL_REPO,
|
| 309 |
+
cached=cached,
|
| 310 |
+
sources=sources
|
| 311 |
+
)
|
| 312 |
+
|
| 313 |
+
except Exception as e:
|
| 314 |
+
logger.error(f"Chat error: {e}")
|
| 315 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
@app.post("/chat/stream")
|
| 319 |
+
async def chat_stream(request: ChatRequest):
|
| 320 |
+
"""Streaming chat endpoint"""
|
| 321 |
+
if llm_instance is None:
|
| 322 |
+
raise HTTPException(status_code=503, detail="Model not loaded")
|
| 323 |
+
|
| 324 |
+
# Track interaction
|
| 325 |
+
count = increment_interaction()
|
| 326 |
+
session_id = request.session_id or str(uuid.uuid4())
|
| 327 |
+
|
| 328 |
+
async def generate():
|
| 329 |
+
try:
|
| 330 |
+
full_response = ""
|
| 331 |
+
|
| 332 |
+
# Send initial metadata
|
| 333 |
+
yield f"data: {json.dumps({'type': 'start', 'session_id': session_id, 'model': MODEL_REPO, 'interaction_count': count})}\n\n"
|
| 334 |
+
|
| 335 |
+
# Stream tokens
|
| 336 |
+
for token in llm_instance.generate_stream(
|
| 337 |
+
request.message,
|
| 338 |
+
max_tokens=request.max_tokens
|
| 339 |
+
):
|
| 340 |
+
full_response += token
|
| 341 |
+
yield f"data: {json.dumps({'type': 'token', 'content': token})}\n\n"
|
| 342 |
+
await asyncio.sleep(0)
|
| 343 |
+
|
| 344 |
+
# Log interaction
|
| 345 |
+
log_interaction(session_id, request.message, len(full_response))
|
| 346 |
+
|
| 347 |
+
yield f"data: {json.dumps({'type': 'end'})}\n\n"
|
| 348 |
+
|
| 349 |
+
except Exception as e:
|
| 350 |
+
yield f"data: {json.dumps({'type': 'error', 'message': str(e)})}\n\n"
|
| 351 |
+
|
| 352 |
+
return StreamingResponse(generate(), media_type="text/event-stream")
|
| 353 |
+
|
| 354 |
+
|
| 355 |
+
@app.websocket("/ws/chat")
|
| 356 |
+
async def websocket_chat(websocket: WebSocket):
|
| 357 |
+
"""WebSocket endpoint for real-time chat"""
|
| 358 |
+
await websocket.accept()
|
| 359 |
+
|
| 360 |
+
if llm_instance is None:
|
| 361 |
+
await websocket.send_json({"type": "error", "message": "Model not loaded"})
|
| 362 |
+
await websocket.close()
|
| 363 |
+
return
|
| 364 |
+
|
| 365 |
+
session_id = str(uuid.uuid4())
|
| 366 |
+
|
| 367 |
+
try:
|
| 368 |
+
await websocket.send_json({
|
| 369 |
+
"type": "connected",
|
| 370 |
+
"session_id": session_id,
|
| 371 |
+
"model": MODEL_REPO
|
| 372 |
+
})
|
| 373 |
+
|
| 374 |
+
while True:
|
| 375 |
+
# Receive message
|
| 376 |
+
data = await websocket.receive_text()
|
| 377 |
+
request = json.loads(data)
|
| 378 |
+
|
| 379 |
+
# Send acknowledgment
|
| 380 |
+
await websocket.send_json({
|
| 381 |
+
"type": "acknowledged",
|
| 382 |
+
"session_id": session_id
|
| 383 |
+
})
|
| 384 |
+
|
| 385 |
+
# Generate and stream response
|
| 386 |
+
full_response = ""
|
| 387 |
+
|
| 388 |
+
for token in llm_instance.generate_stream(request.get('message', '')):
|
| 389 |
+
full_response += token
|
| 390 |
+
await websocket.send_json({
|
| 391 |
+
"type": "token",
|
| 392 |
+
"content": token
|
| 393 |
+
})
|
| 394 |
+
await asyncio.sleep(0)
|
| 395 |
+
|
| 396 |
+
# Send completion
|
| 397 |
+
await websocket.send_json({
|
| 398 |
+
"type": "complete",
|
| 399 |
+
"full_response": full_response
|
| 400 |
+
})
|
| 401 |
+
|
| 402 |
+
except WebSocketDisconnect:
|
| 403 |
+
if session_id in sessions:
|
| 404 |
+
del sessions[session_id]
|
| 405 |
+
|
| 406 |
+
|
| 407 |
+
@app.get("/sessions/{session_id}")
|
| 408 |
+
async def get_session(session_id: str):
|
| 409 |
+
"""Retrieve session history"""
|
| 410 |
+
if session_id not in sessions:
|
| 411 |
+
raise HTTPException(status_code=404, detail="Session not found")
|
| 412 |
+
|
| 413 |
+
return {
|
| 414 |
+
"session_id": session_id,
|
| 415 |
+
"messages": sessions[session_id],
|
| 416 |
+
"model": MODEL_REPO
|
| 417 |
+
}
|
| 418 |
+
|
| 419 |
+
|
| 420 |
+
@app.delete("/sessions/{session_id}")
|
| 421 |
+
async def clear_session(session_id: str):
|
| 422 |
+
"""Clear session history"""
|
| 423 |
+
if session_id in sessions:
|
| 424 |
+
del sessions[session_id]
|
| 425 |
+
|
| 426 |
+
return {"message": "Session cleared"}
|
| 427 |
+
|
| 428 |
+
|
| 429 |
+
@app.get("/interactions/count")
|
| 430 |
+
async def get_interactions_count():
|
| 431 |
+
"""Get total interaction count"""
|
| 432 |
+
count = get_interaction_count()
|
| 433 |
+
return {"count": count}
|
| 434 |
+
|
| 435 |
+
|
| 436 |
+
@app.get("/metrics")
|
| 437 |
+
async def get_metrics():
|
| 438 |
+
"""Get performance metrics"""
|
| 439 |
+
metrics = {
|
| 440 |
+
"model": MODEL_REPO,
|
| 441 |
+
"sessions_active": len(sessions),
|
| 442 |
+
"total_messages": sum(len(s) for s in sessions.values()),
|
| 443 |
+
"total_interactions": get_interaction_count()
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
+
if optimizer:
|
| 447 |
+
metrics["cache"] = optimizer.get_metrics()
|
| 448 |
+
|
| 449 |
+
if memory_manager:
|
| 450 |
+
metrics["memory"] = memory_manager.check_memory()
|
| 451 |
+
|
| 452 |
+
return metrics
|
| 453 |
+
|
| 454 |
+
|
| 455 |
+
@app.post("/cache/clear")
|
| 456 |
+
async def clear_cache():
|
| 457 |
+
"""Clear response cache"""
|
| 458 |
+
if not CACHE_ENABLED or not optimizer:
|
| 459 |
+
raise HTTPException(status_code=400, detail="Cache not enabled")
|
| 460 |
+
|
| 461 |
+
optimizer.clear_cache()
|
| 462 |
+
return {"message": "Cache cleared"}
|
| 463 |
+
|
| 464 |
+
|
| 465 |
+
@app.get("/test")
|
| 466 |
+
async def serve_test_interface():
|
| 467 |
+
"""Serve the test interface HTML"""
|
| 468 |
+
return FileResponse("test_interface.html")
|
| 469 |
+
|
| 470 |
+
|
| 471 |
+
if __name__ == "__main__":
|
| 472 |
+
import uvicorn
|
| 473 |
+
|
| 474 |
+
uvicorn.run(
|
| 475 |
+
app,
|
| 476 |
+
host="0.0.0.0",
|
| 477 |
+
port=8000,
|
| 478 |
+
log_level="info",
|
| 479 |
+
access_log=True
|
| 480 |
+
)
|
monitoring.py
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import time
|
| 3 |
+
from datetime import datetime, timedelta
|
| 4 |
+
from typing import Dict, Any, List
|
| 5 |
+
import json
|
| 6 |
+
import asyncio
|
| 7 |
+
from dataclasses import dataclass, asdict
|
| 8 |
+
import psutil
|
| 9 |
+
from collections import deque
|
| 10 |
+
|
| 11 |
+
# Configure structured logging
|
| 12 |
+
logging.basicConfig(
|
| 13 |
+
level=logging.INFO,
|
| 14 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
| 15 |
+
handlers=[
|
| 16 |
+
logging.FileHandler('logs/chatbot.log'),
|
| 17 |
+
logging.StreamHandler()
|
| 18 |
+
]
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
logger = logging.getLogger(__name__)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
@dataclass
|
| 25 |
+
class RequestMetric:
|
| 26 |
+
timestamp: datetime
|
| 27 |
+
endpoint: str
|
| 28 |
+
response_time: float
|
| 29 |
+
status_code: int
|
| 30 |
+
prompt_length: int
|
| 31 |
+
response_length: int
|
| 32 |
+
cached: bool
|
| 33 |
+
session_id: str
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class PerformanceMonitor:
|
| 37 |
+
def __init__(self, window_size: int = 1000):
|
| 38 |
+
"""Initialize performance monitoring"""
|
| 39 |
+
|
| 40 |
+
self.window_size = window_size
|
| 41 |
+
self.request_metrics = deque(maxlen=window_size)
|
| 42 |
+
self.start_time = datetime.now()
|
| 43 |
+
|
| 44 |
+
# Real-time metrics
|
| 45 |
+
self.metrics = {
|
| 46 |
+
"total_requests": 0,
|
| 47 |
+
"successful_requests": 0,
|
| 48 |
+
"failed_requests": 0,
|
| 49 |
+
"cache_hits": 0,
|
| 50 |
+
"cache_misses": 0,
|
| 51 |
+
"average_response_time": 0,
|
| 52 |
+
"p95_response_time": 0,
|
| 53 |
+
"p99_response_time": 0,
|
| 54 |
+
"requests_per_minute": 0,
|
| 55 |
+
"active_sessions": set(),
|
| 56 |
+
"uptime_hours": 0
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
# System metrics
|
| 60 |
+
self.system_metrics = {
|
| 61 |
+
"cpu_percent": 0,
|
| 62 |
+
"memory_mb": 0,
|
| 63 |
+
"memory_percent": 0,
|
| 64 |
+
"disk_usage_percent": 0
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
def log_request(self, metric: RequestMetric):
|
| 68 |
+
"""Log request metric"""
|
| 69 |
+
|
| 70 |
+
self.request_metrics.append(metric)
|
| 71 |
+
self.metrics["total_requests"] += 1
|
| 72 |
+
|
| 73 |
+
if metric.status_code == 200:
|
| 74 |
+
self.metrics["successful_requests"] += 1
|
| 75 |
+
else:
|
| 76 |
+
self.metrics["failed_requests"] += 1
|
| 77 |
+
|
| 78 |
+
if metric.cached:
|
| 79 |
+
self.metrics["cache_hits"] += 1
|
| 80 |
+
else:
|
| 81 |
+
self.metrics["cache_misses"] += 1
|
| 82 |
+
|
| 83 |
+
self.metrics["active_sessions"].add(metric.session_id)
|
| 84 |
+
|
| 85 |
+
# Log to file
|
| 86 |
+
logger.info(f"Request: {json.dumps(asdict(metric), default=str)}")
|
| 87 |
+
|
| 88 |
+
# Update aggregated metrics
|
| 89 |
+
self._update_aggregates()
|
| 90 |
+
|
| 91 |
+
def _update_aggregates(self):
|
| 92 |
+
"""Update aggregated metrics"""
|
| 93 |
+
|
| 94 |
+
if not self.request_metrics:
|
| 95 |
+
return
|
| 96 |
+
|
| 97 |
+
# Response time percentiles
|
| 98 |
+
response_times = sorted([m.response_time for m in self.request_metrics])
|
| 99 |
+
|
| 100 |
+
self.metrics["average_response_time"] = sum(response_times) / len(response_times)
|
| 101 |
+
|
| 102 |
+
p95_idx = int(len(response_times) * 0.95)
|
| 103 |
+
p99_idx = int(len(response_times) * 0.99)
|
| 104 |
+
|
| 105 |
+
self.metrics["p95_response_time"] = response_times[min(p95_idx, len(response_times) - 1)]
|
| 106 |
+
self.metrics["p99_response_time"] = response_times[min(p99_idx, len(response_times) - 1)]
|
| 107 |
+
|
| 108 |
+
# Requests per minute
|
| 109 |
+
now = datetime.now()
|
| 110 |
+
recent_requests = [
|
| 111 |
+
m for m in self.request_metrics
|
| 112 |
+
if (now - m.timestamp).total_seconds() < 60
|
| 113 |
+
]
|
| 114 |
+
self.metrics["requests_per_minute"] = len(recent_requests)
|
| 115 |
+
|
| 116 |
+
# Uptime
|
| 117 |
+
self.metrics["uptime_hours"] = (now - self.start_time).total_seconds() / 3600
|
| 118 |
+
|
| 119 |
+
# Cache hit rate
|
| 120 |
+
if self.metrics["total_requests"] > 0:
|
| 121 |
+
self.metrics["cache_hit_rate"] = (
|
| 122 |
+
self.metrics["cache_hits"] / self.metrics["total_requests"]
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
def update_system_metrics(self):
|
| 126 |
+
"""Update system resource metrics"""
|
| 127 |
+
|
| 128 |
+
process = psutil.Process()
|
| 129 |
+
|
| 130 |
+
self.system_metrics["cpu_percent"] = process.cpu_percent()
|
| 131 |
+
self.system_metrics["memory_mb"] = process.memory_info().rss / 1024 / 1024
|
| 132 |
+
self.system_metrics["memory_percent"] = process.memory_percent()
|
| 133 |
+
|
| 134 |
+
disk = psutil.disk_usage('/')
|
| 135 |
+
self.system_metrics["disk_usage_percent"] = disk.percent
|
| 136 |
+
|
| 137 |
+
return self.system_metrics
|
| 138 |
+
|
| 139 |
+
def get_dashboard_metrics(self) -> Dict[str, Any]:
|
| 140 |
+
"""Get metrics for dashboard display"""
|
| 141 |
+
|
| 142 |
+
self.update_system_metrics()
|
| 143 |
+
|
| 144 |
+
return {
|
| 145 |
+
"performance": self.metrics,
|
| 146 |
+
"system": self.system_metrics,
|
| 147 |
+
"health_score": self._calculate_health_score()
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
def _calculate_health_score(self) -> float:
|
| 151 |
+
"""Calculate overall system health score (0-100)"""
|
| 152 |
+
|
| 153 |
+
score = 100.0
|
| 154 |
+
|
| 155 |
+
# Deduct for high response times
|
| 156 |
+
if self.metrics["average_response_time"] > 5:
|
| 157 |
+
score -= 20
|
| 158 |
+
elif self.metrics["average_response_time"] > 2:
|
| 159 |
+
score -= 10
|
| 160 |
+
|
| 161 |
+
# Deduct for errors
|
| 162 |
+
error_rate = self.metrics["failed_requests"] / max(self.metrics["total_requests"], 1)
|
| 163 |
+
score -= error_rate * 50
|
| 164 |
+
|
| 165 |
+
# Deduct for high memory usage
|
| 166 |
+
if self.system_metrics["memory_percent"] > 90:
|
| 167 |
+
score -= 30
|
| 168 |
+
elif self.system_metrics["memory_percent"] > 70:
|
| 169 |
+
score -= 10
|
| 170 |
+
|
| 171 |
+
# Deduct for low cache hit rate
|
| 172 |
+
cache_hit_rate = self.metrics.get("cache_hit_rate", 0)
|
| 173 |
+
if cache_hit_rate < 0.3:
|
| 174 |
+
score -= 10
|
| 175 |
+
|
| 176 |
+
return max(0, min(100, score))
|
| 177 |
+
|
| 178 |
+
def generate_report(self) -> str:
|
| 179 |
+
"""Generate performance report"""
|
| 180 |
+
|
| 181 |
+
report = f"""
|
| 182 |
+
CYBERSECURITY CHATBOT PERFORMANCE REPORT
|
| 183 |
+
=========================================
|
| 184 |
+
Generated: {datetime.now().isoformat()}
|
| 185 |
+
Uptime: {self.metrics['uptime_hours']:.2f} hours
|
| 186 |
+
|
| 187 |
+
REQUEST METRICS
|
| 188 |
+
---------------
|
| 189 |
+
Total Requests: {self.metrics['total_requests']}
|
| 190 |
+
Successful: {self.metrics['successful_requests']}
|
| 191 |
+
Failed: {self.metrics['failed_requests']}
|
| 192 |
+
Error Rate: {(self.metrics['failed_requests'] / max(self.metrics['total_requests'], 1) * 100):.2f}%
|
| 193 |
+
|
| 194 |
+
PERFORMANCE
|
| 195 |
+
-----------
|
| 196 |
+
Average Response Time: {self.metrics['average_response_time']:.3f}s
|
| 197 |
+
P95 Response Time: {self.metrics['p95_response_time']:.3f}s
|
| 198 |
+
P99 Response Time: {self.metrics['p99_response_time']:.3f}s
|
| 199 |
+
Requests/Minute: {self.metrics['requests_per_minute']}
|
| 200 |
+
|
| 201 |
+
CACHE PERFORMANCE
|
| 202 |
+
-----------------
|
| 203 |
+
Cache Hits: {self.metrics['cache_hits']}
|
| 204 |
+
Cache Misses: {self.metrics['cache_misses']}
|
| 205 |
+
Hit Rate: {self.metrics.get('cache_hit_rate', 0) * 100:.2f}%
|
| 206 |
+
|
| 207 |
+
SYSTEM RESOURCES
|
| 208 |
+
----------------
|
| 209 |
+
CPU Usage: {self.system_metrics['cpu_percent']:.1f}%
|
| 210 |
+
Memory Usage: {self.system_metrics['memory_mb']:.2f} MB ({self.system_metrics['memory_percent']:.1f}%)
|
| 211 |
+
Disk Usage: {self.system_metrics['disk_usage_percent']:.1f}%
|
| 212 |
+
|
| 213 |
+
HEALTH SCORE: {self._calculate_health_score():.1f}/100
|
| 214 |
+
"""
|
| 215 |
+
|
| 216 |
+
return report
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
# Alert system
|
| 220 |
+
class AlertManager:
|
| 221 |
+
def __init__(self, webhook_url: str = None):
|
| 222 |
+
"""Initialize alert manager"""
|
| 223 |
+
|
| 224 |
+
self.webhook_url = webhook_url
|
| 225 |
+
self.alert_thresholds = {
|
| 226 |
+
"response_time": 5.0, # seconds
|
| 227 |
+
"error_rate": 0.1, # 10%
|
| 228 |
+
"memory_percent": 85,
|
| 229 |
+
"cpu_percent": 90
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
self.alert_history = deque(maxlen=100)
|
| 233 |
+
self.last_alert_time = {}
|
| 234 |
+
|
| 235 |
+
def check_alerts(self, metrics: Dict[str, Any]):
|
| 236 |
+
"""Check if any alerts should be triggered"""
|
| 237 |
+
|
| 238 |
+
alerts = []
|
| 239 |
+
|
| 240 |
+
# Check response time
|
| 241 |
+
if metrics["performance"]["average_response_time"] > self.alert_thresholds["response_time"]:
|
| 242 |
+
alerts.append({
|
| 243 |
+
"level": "warning",
|
| 244 |
+
"type": "response_time",
|
| 245 |
+
"message": f"High response time: {metrics['performance']['average_response_time']:.2f}s"
|
| 246 |
+
})
|
| 247 |
+
|
| 248 |
+
# Check error rate
|
| 249 |
+
error_rate = metrics["performance"]["failed_requests"] / max(metrics["performance"]["total_requests"], 1)
|
| 250 |
+
if error_rate > self.alert_thresholds["error_rate"]:
|
| 251 |
+
alerts.append({
|
| 252 |
+
"level": "critical",
|
| 253 |
+
"type": "error_rate",
|
| 254 |
+
"message": f"High error rate: {error_rate * 100:.2f}%"
|
| 255 |
+
})
|
| 256 |
+
|
| 257 |
+
# Check memory
|
| 258 |
+
if metrics["system"]["memory_percent"] > self.alert_thresholds["memory_percent"]:
|
| 259 |
+
alerts.append({
|
| 260 |
+
"level": "warning",
|
| 261 |
+
"type": "memory",
|
| 262 |
+
"message": f"High memory usage: {metrics['system']['memory_percent']:.1f}%"
|
| 263 |
+
})
|
| 264 |
+
|
| 265 |
+
# Check CPU
|
| 266 |
+
if metrics["system"]["cpu_percent"] > self.alert_thresholds["cpu_percent"]:
|
| 267 |
+
alerts.append({
|
| 268 |
+
"level": "warning",
|
| 269 |
+
"type": "cpu",
|
| 270 |
+
"message": f"High CPU usage: {metrics['system']['cpu_percent']:.1f}%"
|
| 271 |
+
})
|
| 272 |
+
|
| 273 |
+
# Send alerts
|
| 274 |
+
for alert in alerts:
|
| 275 |
+
self._send_alert(alert)
|
| 276 |
+
|
| 277 |
+
def _send_alert(self, alert: Dict[str, Any]):
|
| 278 |
+
"""Send alert notification"""
|
| 279 |
+
|
| 280 |
+
# Rate limiting - don't send same alert more than once per 5 minutes
|
| 281 |
+
alert_key = f"{alert['type']}_{alert['level']}"
|
| 282 |
+
now = datetime.now()
|
| 283 |
+
|
| 284 |
+
if alert_key in self.last_alert_time:
|
| 285 |
+
if (now - self.last_alert_time[alert_key]).seconds < 300:
|
| 286 |
+
return
|
| 287 |
+
|
| 288 |
+
self.last_alert_time[alert_key] = now
|
| 289 |
+
self.alert_history.append({
|
| 290 |
+
"timestamp": now.isoformat(),
|
| 291 |
+
**alert
|
| 292 |
+
})
|
| 293 |
+
|
| 294 |
+
# Log alert
|
| 295 |
+
if alert["level"] == "critical":
|
| 296 |
+
logger.error(f"ALERT: {alert['message']}")
|
| 297 |
+
else:
|
| 298 |
+
logger.warning(f"ALERT: {alert['message']}")
|
| 299 |
+
|
| 300 |
+
# Send to webhook if configured
|
| 301 |
+
if self.webhook_url:
|
| 302 |
+
self._send_webhook(alert)
|
optimisations.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import hashlib
|
| 3 |
+
import time
|
| 4 |
+
import psutil
|
| 5 |
+
import gc
|
| 6 |
+
from typing import Dict, List, Optional, Any
|
| 7 |
+
from collections import OrderedDict
|
| 8 |
+
from dataclasses import dataclass
|
| 9 |
+
from datetime import datetime, timedelta
|
| 10 |
+
import redis
|
| 11 |
+
import json
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@dataclass
|
| 15 |
+
class CacheEntry:
|
| 16 |
+
response: str
|
| 17 |
+
timestamp: datetime
|
| 18 |
+
hit_count: int = 0
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class PerformanceOptimizer:
|
| 22 |
+
def __init__(self,
|
| 23 |
+
cache_size: int = 100,
|
| 24 |
+
cache_ttl_hours: int = 24,
|
| 25 |
+
use_redis: bool = False):
|
| 26 |
+
"""Initialize performance optimizer with caching"""
|
| 27 |
+
|
| 28 |
+
self.cache_size = cache_size
|
| 29 |
+
self.cache_ttl = timedelta(hours=cache_ttl_hours)
|
| 30 |
+
|
| 31 |
+
# Use Redis if available, fallback to in-memory
|
| 32 |
+
self.use_redis = use_redis
|
| 33 |
+
if use_redis:
|
| 34 |
+
try:
|
| 35 |
+
self.redis_client = redis.Redis(
|
| 36 |
+
host='localhost',
|
| 37 |
+
port=6379,
|
| 38 |
+
decode_responses=True
|
| 39 |
+
)
|
| 40 |
+
self.redis_client.ping()
|
| 41 |
+
except:
|
| 42 |
+
print("Redis not available, using in-memory cache")
|
| 43 |
+
self.use_redis = False
|
| 44 |
+
self.cache = OrderedDict()
|
| 45 |
+
else:
|
| 46 |
+
self.cache = OrderedDict()
|
| 47 |
+
|
| 48 |
+
# Metrics
|
| 49 |
+
self.metrics = {
|
| 50 |
+
"cache_hits": 0,
|
| 51 |
+
"cache_misses": 0,
|
| 52 |
+
"total_requests": 0,
|
| 53 |
+
"average_response_time": 0,
|
| 54 |
+
"memory_usage_mb": 0
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
def _hash_prompt(self, prompt: str) -> str:
|
| 58 |
+
"""Create hash for caching"""
|
| 59 |
+
normalized = prompt.lower().strip()
|
| 60 |
+
return hashlib.md5(normalized.encode()).hexdigest()
|
| 61 |
+
|
| 62 |
+
def get_cached_response(self, prompt: str) -> Optional[str]:
|
| 63 |
+
"""Get response from cache if available"""
|
| 64 |
+
|
| 65 |
+
self.metrics["total_requests"] += 1
|
| 66 |
+
prompt_hash = self._hash_prompt(prompt)
|
| 67 |
+
|
| 68 |
+
if self.use_redis:
|
| 69 |
+
cached = self.redis_client.get(f"chat:{prompt_hash}")
|
| 70 |
+
if cached:
|
| 71 |
+
self.metrics["cache_hits"] += 1
|
| 72 |
+
# Update hit count
|
| 73 |
+
self.redis_client.hincrby(f"chat:stats:{prompt_hash}", "hits", 1)
|
| 74 |
+
return json.loads(cached)["response"]
|
| 75 |
+
else:
|
| 76 |
+
if prompt_hash in self.cache:
|
| 77 |
+
entry = self.cache[prompt_hash]
|
| 78 |
+
# Check TTL
|
| 79 |
+
if datetime.now() - entry.timestamp < self.cache_ttl:
|
| 80 |
+
self.metrics["cache_hits"] += 1
|
| 81 |
+
entry.hit_count += 1
|
| 82 |
+
# Move to end (LRU)
|
| 83 |
+
self.cache.move_to_end(prompt_hash)
|
| 84 |
+
return entry.response
|
| 85 |
+
else:
|
| 86 |
+
# Expired
|
| 87 |
+
del self.cache[prompt_hash]
|
| 88 |
+
|
| 89 |
+
self.metrics["cache_misses"] += 1
|
| 90 |
+
return None
|
| 91 |
+
|
| 92 |
+
def cache_response(self, prompt: str, response: str):
|
| 93 |
+
"""Cache a response"""
|
| 94 |
+
|
| 95 |
+
prompt_hash = self._hash_prompt(prompt)
|
| 96 |
+
|
| 97 |
+
if self.use_redis:
|
| 98 |
+
cache_data = {
|
| 99 |
+
"response": response,
|
| 100 |
+
"timestamp": datetime.now().isoformat()
|
| 101 |
+
}
|
| 102 |
+
self.redis_client.setex(
|
| 103 |
+
f"chat:{prompt_hash}",
|
| 104 |
+
int(self.cache_ttl.total_seconds()),
|
| 105 |
+
json.dumps(cache_data)
|
| 106 |
+
)
|
| 107 |
+
self.redis_client.hset(
|
| 108 |
+
f"chat:stats:{prompt_hash}",
|
| 109 |
+
mapping={"hits": 0, "created": datetime.now().isoformat()}
|
| 110 |
+
)
|
| 111 |
+
else:
|
| 112 |
+
# LRU cache management
|
| 113 |
+
if len(self.cache) >= self.cache_size:
|
| 114 |
+
# Remove least recently used
|
| 115 |
+
self.cache.popitem(last=False)
|
| 116 |
+
|
| 117 |
+
self.cache[prompt_hash] = CacheEntry(
|
| 118 |
+
response=response,
|
| 119 |
+
timestamp=datetime.now()
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
def get_metrics(self) -> Dict[str, Any]:
|
| 123 |
+
"""Get performance metrics"""
|
| 124 |
+
|
| 125 |
+
# Update memory usage
|
| 126 |
+
process = psutil.Process()
|
| 127 |
+
self.metrics["memory_usage_mb"] = process.memory_info().rss / 1024 / 1024
|
| 128 |
+
|
| 129 |
+
# Calculate cache hit rate
|
| 130 |
+
if self.metrics["total_requests"] > 0:
|
| 131 |
+
self.metrics["cache_hit_rate"] = (
|
| 132 |
+
self.metrics["cache_hits"] / self.metrics["total_requests"]
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
return self.metrics
|
| 136 |
+
|
| 137 |
+
def clear_cache(self):
|
| 138 |
+
"""Clear all cached responses"""
|
| 139 |
+
|
| 140 |
+
if self.use_redis:
|
| 141 |
+
for key in self.redis_client.scan_iter("chat:*"):
|
| 142 |
+
self.redis_client.delete(key)
|
| 143 |
+
else:
|
| 144 |
+
self.cache.clear()
|
| 145 |
+
|
| 146 |
+
gc.collect()
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
class MemoryManager:
|
| 150 |
+
def __init__(self, max_memory_mb: int = 8192):
|
| 151 |
+
"""Initialize memory manager"""
|
| 152 |
+
|
| 153 |
+
self.max_memory_mb = max_memory_mb
|
| 154 |
+
self.warning_threshold = 0.8 # Warn at 80% usage
|
| 155 |
+
self.critical_threshold = 0.9 # Critical at 90% usage
|
| 156 |
+
|
| 157 |
+
def check_memory(self) -> Dict[str, Any]:
|
| 158 |
+
"""Check current memory usage"""
|
| 159 |
+
|
| 160 |
+
process = psutil.Process()
|
| 161 |
+
memory_info = process.memory_info()
|
| 162 |
+
|
| 163 |
+
current_mb = memory_info.rss / 1024 / 1024
|
| 164 |
+
percentage = current_mb / self.max_memory_mb
|
| 165 |
+
|
| 166 |
+
status = "normal"
|
| 167 |
+
if percentage > self.critical_threshold:
|
| 168 |
+
status = "critical"
|
| 169 |
+
elif percentage > self.warning_threshold:
|
| 170 |
+
status = "warning"
|
| 171 |
+
|
| 172 |
+
return {
|
| 173 |
+
"current_mb": round(current_mb, 2),
|
| 174 |
+
"max_mb": self.max_memory_mb,
|
| 175 |
+
"percentage": round(percentage * 100, 2),
|
| 176 |
+
"status": status,
|
| 177 |
+
"available_mb": round(self.max_memory_mb - current_mb, 2)
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
def optimize_if_needed(self) -> bool:
|
| 181 |
+
"""Run optimization if memory usage is high"""
|
| 182 |
+
|
| 183 |
+
memory_status = self.check_memory()
|
| 184 |
+
|
| 185 |
+
if memory_status["status"] in ["warning", "critical"]:
|
| 186 |
+
# Force garbage collection
|
| 187 |
+
gc.collect()
|
| 188 |
+
|
| 189 |
+
# Clear unused objects
|
| 190 |
+
if memory_status["status"] == "critical":
|
| 191 |
+
# More aggressive cleanup
|
| 192 |
+
gc.collect(2)
|
| 193 |
+
|
| 194 |
+
return True
|
| 195 |
+
|
| 196 |
+
return False
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
class RequestBatcher:
|
| 200 |
+
def __init__(self, batch_size: int = 5, timeout_ms: int = 100):
|
| 201 |
+
"""Initialize request batcher for efficiency"""
|
| 202 |
+
|
| 203 |
+
self.batch_size = batch_size
|
| 204 |
+
self.timeout_ms = timeout_ms
|
| 205 |
+
self.pending_requests = []
|
| 206 |
+
self.results = {}
|
| 207 |
+
|
| 208 |
+
async def add_request(self, request_id: str, prompt: str) -> str:
|
| 209 |
+
"""Add request to batch"""
|
| 210 |
+
|
| 211 |
+
self.pending_requests.append({
|
| 212 |
+
"id": request_id,
|
| 213 |
+
"prompt": prompt,
|
| 214 |
+
"timestamp": time.time()
|
| 215 |
+
})
|
| 216 |
+
|
| 217 |
+
# Process if batch is full
|
| 218 |
+
if len(self.pending_requests) >= self.batch_size:
|
| 219 |
+
await self._process_batch()
|
| 220 |
+
else:
|
| 221 |
+
# Wait for timeout
|
| 222 |
+
await asyncio.sleep(self.timeout_ms / 1000)
|
| 223 |
+
if request_id not in self.results:
|
| 224 |
+
await self._process_batch()
|
| 225 |
+
|
| 226 |
+
return self.results.get(request_id, "Error processing request")
|
| 227 |
+
|
| 228 |
+
async def _process_batch(self):
|
| 229 |
+
"""Process pending requests as batch"""
|
| 230 |
+
|
| 231 |
+
if not self.pending_requests:
|
| 232 |
+
return
|
| 233 |
+
|
| 234 |
+
batch = self.pending_requests[:self.batch_size]
|
| 235 |
+
self.pending_requests = self.pending_requests[self.batch_size:]
|
| 236 |
+
|
| 237 |
+
# Process batch (simulate concurrent processing)
|
| 238 |
+
tasks = []
|
| 239 |
+
for request in batch:
|
| 240 |
+
# In production, this would call the LLM
|
| 241 |
+
tasks.append(self._process_single(request))
|
| 242 |
+
|
| 243 |
+
results = await asyncio.gather(*tasks)
|
| 244 |
+
|
| 245 |
+
for request, result in zip(batch, results):
|
| 246 |
+
self.results[request["id"]] = result
|
| 247 |
+
|
| 248 |
+
async def _process_single(self, request: Dict[str, Any]) -> str:
|
| 249 |
+
"""Process single request (placeholder)"""
|
| 250 |
+
|
| 251 |
+
# Simulate processing
|
| 252 |
+
await asyncio.sleep(0.1)
|
| 253 |
+
return f"Response to: {request['prompt']}"
|
requirements.txt
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
accelerate==1.9.0
|
| 2 |
+
aiofiles==24.1.0
|
| 3 |
+
annotated-types==0.7.0
|
| 4 |
+
anyio==4.10.0
|
| 5 |
+
attrs==25.3.0
|
| 6 |
+
backoff==2.2.1
|
| 7 |
+
bcrypt==4.3.0
|
| 8 |
+
build==1.3.0
|
| 9 |
+
cachetools==5.5.2
|
| 10 |
+
certifi==2025.8.3
|
| 11 |
+
charset-normalizer==3.4.2
|
| 12 |
+
chromadb==1.0.15
|
| 13 |
+
click==8.2.1
|
| 14 |
+
coloredlogs==15.0.1
|
| 15 |
+
diskcache==5.6.3
|
| 16 |
+
distro==1.9.0
|
| 17 |
+
durationpy==0.10
|
| 18 |
+
fastapi==0.116.1
|
| 19 |
+
filelock==3.18.0
|
| 20 |
+
flatbuffers==25.2.10
|
| 21 |
+
fsspec==2025.7.0
|
| 22 |
+
google-auth==2.40.3
|
| 23 |
+
googleapis-common-protos==1.70.0
|
| 24 |
+
grpcio==1.74.0
|
| 25 |
+
h11==0.16.0
|
| 26 |
+
hf-xet==1.1.7
|
| 27 |
+
httpcore==1.0.9
|
| 28 |
+
httptools==0.6.4
|
| 29 |
+
httpx==0.28.1
|
| 30 |
+
huggingface-hub==0.34.3
|
| 31 |
+
humanfriendly==10.0
|
| 32 |
+
idna==3.10
|
| 33 |
+
importlib_metadata==8.7.0
|
| 34 |
+
importlib_resources==6.5.2
|
| 35 |
+
Jinja2==3.1.6
|
| 36 |
+
joblib==1.5.1
|
| 37 |
+
jsonschema==4.25.0
|
| 38 |
+
jsonschema-specifications==2025.4.1
|
| 39 |
+
kubernetes==33.1.0
|
| 40 |
+
llama_cpp_python==0.3.14
|
| 41 |
+
markdown-it-py==3.0.0
|
| 42 |
+
MarkupSafe==3.0.2
|
| 43 |
+
mdurl==0.1.2
|
| 44 |
+
mmh3==5.2.0
|
| 45 |
+
mpmath==1.3.0
|
| 46 |
+
networkx==3.5
|
| 47 |
+
numpy==2.3.2
|
| 48 |
+
oauthlib==3.3.1
|
| 49 |
+
onnxruntime==1.22.1
|
| 50 |
+
opentelemetry-api==1.36.0
|
| 51 |
+
opentelemetry-exporter-otlp-proto-common==1.36.0
|
| 52 |
+
opentelemetry-exporter-otlp-proto-grpc==1.36.0
|
| 53 |
+
opentelemetry-proto==1.36.0
|
| 54 |
+
opentelemetry-sdk==1.36.0
|
| 55 |
+
opentelemetry-semantic-conventions==0.57b0
|
| 56 |
+
orjson==3.11.1
|
| 57 |
+
overrides==7.7.0
|
| 58 |
+
packaging==25.0
|
| 59 |
+
pillow==11.3.0
|
| 60 |
+
posthog==5.4.0
|
| 61 |
+
protobuf==6.31.1
|
| 62 |
+
psutil==7.0.0
|
| 63 |
+
pyasn1==0.6.1
|
| 64 |
+
pyasn1_modules==0.4.2
|
| 65 |
+
pybase64==1.4.2
|
| 66 |
+
pydantic==2.11.7
|
| 67 |
+
pydantic_core==2.33.2
|
| 68 |
+
Pygments==2.19.2
|
| 69 |
+
PyPika==0.48.9
|
| 70 |
+
pyproject_hooks==1.2.0
|
| 71 |
+
python-dateutil==2.9.0.post0
|
| 72 |
+
python-dotenv==1.1.1
|
| 73 |
+
PyYAML==6.0.2
|
| 74 |
+
redis==6.3.0
|
| 75 |
+
referencing==0.36.2
|
| 76 |
+
regex==2025.7.34
|
| 77 |
+
requests==2.32.4
|
| 78 |
+
requests-oauthlib==2.0.0
|
| 79 |
+
rich==14.1.0
|
| 80 |
+
rpds-py==0.26.0
|
| 81 |
+
rsa==4.9.1
|
| 82 |
+
safetensors==0.5.3
|
| 83 |
+
scikit-learn==1.7.1
|
| 84 |
+
scipy==1.16.1
|
| 85 |
+
sentence-transformers==5.0.0
|
| 86 |
+
sentencepiece==0.2.0
|
| 87 |
+
setuptools==80.9.0
|
| 88 |
+
shellingham==1.5.4
|
| 89 |
+
six==1.17.0
|
| 90 |
+
sniffio==1.3.1
|
| 91 |
+
starlette==0.47.2
|
| 92 |
+
sympy==1.14.0
|
| 93 |
+
tenacity==9.1.2
|
| 94 |
+
threadpoolctl==3.6.0
|
| 95 |
+
tokenizers==0.21.4
|
| 96 |
+
torch==2.7.1
|
| 97 |
+
tqdm==4.67.1
|
| 98 |
+
transformers==4.55.0
|
| 99 |
+
typer==0.16.0
|
| 100 |
+
typing-inspection==0.4.1
|
| 101 |
+
typing_extensions==4.14.1
|
| 102 |
+
urllib3==2.5.0
|
| 103 |
+
uvicorn==0.35.0
|
| 104 |
+
uvloop==0.21.0
|
| 105 |
+
watchfiles==1.1.0
|
| 106 |
+
websocket-client==1.8.0
|
| 107 |
+
websockets==15.0.1
|
| 108 |
+
zipp==3.23.0
|
test_interface.html
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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>Cybersecurity Chatbot Test</title>
|
| 7 |
+
<style>
|
| 8 |
+
* {
|
| 9 |
+
margin: 0;
|
| 10 |
+
padding: 0;
|
| 11 |
+
box-sizing: border-box;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
body {
|
| 15 |
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
| 16 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 17 |
+
min-height: 100vh;
|
| 18 |
+
display: flex;
|
| 19 |
+
justify-content: center;
|
| 20 |
+
align-items: center;
|
| 21 |
+
padding: 20px;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
.container {
|
| 25 |
+
background: white;
|
| 26 |
+
border-radius: 20px;
|
| 27 |
+
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
| 28 |
+
width: 100%;
|
| 29 |
+
max-width: 800px;
|
| 30 |
+
height: 600px;
|
| 31 |
+
display: flex;
|
| 32 |
+
flex-direction: column;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
.header {
|
| 36 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 37 |
+
color: white;
|
| 38 |
+
padding: 20px;
|
| 39 |
+
border-radius: 20px 20px 0 0;
|
| 40 |
+
text-align: center;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
.header h1 {
|
| 44 |
+
font-size: 24px;
|
| 45 |
+
margin-bottom: 5px;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
.header p {
|
| 49 |
+
opacity: 0.9;
|
| 50 |
+
font-size: 14px;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
.chat-container {
|
| 54 |
+
flex: 1;
|
| 55 |
+
overflow-y: auto;
|
| 56 |
+
padding: 20px;
|
| 57 |
+
display: flex;
|
| 58 |
+
flex-direction: column;
|
| 59 |
+
gap: 15px;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
.message {
|
| 63 |
+
padding: 12px 16px;
|
| 64 |
+
border-radius: 18px;
|
| 65 |
+
max-width: 70%;
|
| 66 |
+
word-wrap: break-word;
|
| 67 |
+
animation: fadeIn 0.3s ease-in;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
@keyframes fadeIn {
|
| 71 |
+
from { opacity: 0; transform: translateY(10px); }
|
| 72 |
+
to { opacity: 1; transform: translateY(0); }
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
.user-message {
|
| 76 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 77 |
+
color: white;
|
| 78 |
+
align-self: flex-end;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
.assistant-message {
|
| 82 |
+
background: #f3f4f6;
|
| 83 |
+
color: #1f2937;
|
| 84 |
+
align-self: flex-start;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
.typing-indicator {
|
| 88 |
+
display: none;
|
| 89 |
+
align-self: flex-start;
|
| 90 |
+
padding: 12px 16px;
|
| 91 |
+
background: #f3f4f6;
|
| 92 |
+
border-radius: 18px;
|
| 93 |
+
margin-bottom: 10px;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
.typing-indicator span {
|
| 97 |
+
display: inline-block;
|
| 98 |
+
width: 8px;
|
| 99 |
+
height: 8px;
|
| 100 |
+
border-radius: 50%;
|
| 101 |
+
background: #9ca3af;
|
| 102 |
+
margin: 0 2px;
|
| 103 |
+
animation: typing 1.4s infinite;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
|
| 107 |
+
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
|
| 108 |
+
|
| 109 |
+
@keyframes typing {
|
| 110 |
+
0%, 60%, 100% { transform: translateY(0); }
|
| 111 |
+
30% { transform: translateY(-10px); }
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
.input-container {
|
| 115 |
+
padding: 20px;
|
| 116 |
+
border-top: 1px solid #e5e7eb;
|
| 117 |
+
display: flex;
|
| 118 |
+
gap: 10px;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
#messageInput {
|
| 122 |
+
flex: 1;
|
| 123 |
+
padding: 12px 16px;
|
| 124 |
+
border: 2px solid #e5e7eb;
|
| 125 |
+
border-radius: 25px;
|
| 126 |
+
font-size: 14px;
|
| 127 |
+
outline: none;
|
| 128 |
+
transition: border-color 0.3s;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
#messageInput:focus {
|
| 132 |
+
border-color: #667eea;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
.send-button {
|
| 136 |
+
padding: 12px 24px;
|
| 137 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 138 |
+
color: white;
|
| 139 |
+
border: none;
|
| 140 |
+
border-radius: 25px;
|
| 141 |
+
cursor: pointer;
|
| 142 |
+
font-weight: 600;
|
| 143 |
+
transition: transform 0.2s;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.send-button:hover {
|
| 147 |
+
transform: scale(1.05);
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
.send-button:disabled {
|
| 151 |
+
opacity: 0.5;
|
| 152 |
+
cursor: not-allowed;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
.quick-prompts {
|
| 156 |
+
padding: 10px 20px;
|
| 157 |
+
display: flex;
|
| 158 |
+
gap: 10px;
|
| 159 |
+
flex-wrap: wrap;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
.quick-prompt {
|
| 163 |
+
padding: 6px 12px;
|
| 164 |
+
background: #f3f4f6;
|
| 165 |
+
border: 1px solid #e5e7eb;
|
| 166 |
+
border-radius: 15px;
|
| 167 |
+
font-size: 12px;
|
| 168 |
+
cursor: pointer;
|
| 169 |
+
transition: all 0.2s;
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
.quick-prompt:hover {
|
| 173 |
+
background: #e5e7eb;
|
| 174 |
+
transform: translateY(-2px);
|
| 175 |
+
}
|
| 176 |
+
</style>
|
| 177 |
+
</head>
|
| 178 |
+
<body>
|
| 179 |
+
<div class="container">
|
| 180 |
+
<div class="header">
|
| 181 |
+
<h1>🔒 Cybersecurity Assistant</h1>
|
| 182 |
+
<p>Ask me anything about security best practices</p>
|
| 183 |
+
<p style="margin-top: 10px; font-size: 12px; opacity: 0.8;">
|
| 184 |
+
Total Interactions: <span id="interactionCount">0</span>
|
| 185 |
+
</p>
|
| 186 |
+
</div>
|
| 187 |
+
|
| 188 |
+
<div class="quick-prompts">
|
| 189 |
+
<div class="quick-prompt" onclick="sendQuickPrompt('How do I identify phishing emails?')">Phishing Detection</div>
|
| 190 |
+
<div class="quick-prompt" onclick="sendQuickPrompt('What makes a strong password?')">Password Security</div>
|
| 191 |
+
<div class="quick-prompt" onclick="sendQuickPrompt('How do I secure my home WiFi?')">Network Security</div>
|
| 192 |
+
<div class="quick-prompt" onclick="sendQuickPrompt('What should I do if I clicked a suspicious link?')">Incident Response</div>
|
| 193 |
+
</div>
|
| 194 |
+
|
| 195 |
+
<div class="chat-container" id="chatContainer">
|
| 196 |
+
<div class="assistant-message message">
|
| 197 |
+
Hello! I'm your cybersecurity assistant. How can I help you stay safe online today?
|
| 198 |
+
</div>
|
| 199 |
+
</div>
|
| 200 |
+
|
| 201 |
+
<div class="typing-indicator" id="typingIndicator">
|
| 202 |
+
<span></span>
|
| 203 |
+
<span></span>
|
| 204 |
+
<span></span>
|
| 205 |
+
</div>
|
| 206 |
+
|
| 207 |
+
<div class="input-container">
|
| 208 |
+
<input
|
| 209 |
+
type="text"
|
| 210 |
+
id="messageInput"
|
| 211 |
+
placeholder="Ask about cybersecurity..."
|
| 212 |
+
onkeypress="handleKeyPress(event)"
|
| 213 |
+
>
|
| 214 |
+
<button class="send-button" onclick="sendMessage()" id="sendButton">Send</button>
|
| 215 |
+
</div>
|
| 216 |
+
</div>
|
| 217 |
+
|
| 218 |
+
<script>
|
| 219 |
+
let sessionId = null;
|
| 220 |
+
let isProcessing = false;
|
| 221 |
+
|
| 222 |
+
function handleKeyPress(event) {
|
| 223 |
+
if (event.key === 'Enter' && !isProcessing) {
|
| 224 |
+
sendMessage();
|
| 225 |
+
}
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
function sendQuickPrompt(prompt) {
|
| 229 |
+
document.getElementById('messageInput').value = prompt;
|
| 230 |
+
sendMessage();
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
function addMessage(content, isUser = false) {
|
| 234 |
+
const chatContainer = document.getElementById('chatContainer');
|
| 235 |
+
const messageDiv = document.createElement('div');
|
| 236 |
+
messageDiv.className = `message ${isUser ? 'user-message' : 'assistant-message'}`;
|
| 237 |
+
messageDiv.textContent = content;
|
| 238 |
+
chatContainer.appendChild(messageDiv);
|
| 239 |
+
chatContainer.scrollTop = chatContainer.scrollHeight;
|
| 240 |
+
return messageDiv;
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
function showTyping() {
|
| 244 |
+
document.getElementById('typingIndicator').style.display = 'block';
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
function hideTyping() {
|
| 248 |
+
document.getElementById('typingIndicator').style.display = 'none';
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
async function sendMessage() {
|
| 252 |
+
const input = document.getElementById('messageInput');
|
| 253 |
+
const sendButton = document.getElementById('sendButton');
|
| 254 |
+
const message = input.value.trim();
|
| 255 |
+
|
| 256 |
+
if (!message || isProcessing) return;
|
| 257 |
+
|
| 258 |
+
isProcessing = true;
|
| 259 |
+
sendButton.disabled = true;
|
| 260 |
+
|
| 261 |
+
// Add user message
|
| 262 |
+
addMessage(message, true);
|
| 263 |
+
input.value = '';
|
| 264 |
+
|
| 265 |
+
// Show typing indicator
|
| 266 |
+
showTyping();
|
| 267 |
+
|
| 268 |
+
try {
|
| 269 |
+
// Stream response
|
| 270 |
+
const response = await fetch('http://localhost:8000/chat/stream', {
|
| 271 |
+
method: 'POST',
|
| 272 |
+
headers: {
|
| 273 |
+
'Content-Type': 'application/json',
|
| 274 |
+
},
|
| 275 |
+
body: JSON.stringify({
|
| 276 |
+
message: message,
|
| 277 |
+
session_id: sessionId
|
| 278 |
+
})
|
| 279 |
+
});
|
| 280 |
+
|
| 281 |
+
const reader = response.body.getReader();
|
| 282 |
+
const decoder = new TextDecoder();
|
| 283 |
+
let assistantMessage = null;
|
| 284 |
+
let fullResponse = '';
|
| 285 |
+
|
| 286 |
+
while (true) {
|
| 287 |
+
const { done, value } = await reader.read();
|
| 288 |
+
if (done) break;
|
| 289 |
+
|
| 290 |
+
const text = decoder.decode(value);
|
| 291 |
+
const lines = text.split('\n');
|
| 292 |
+
|
| 293 |
+
for (const line of lines) {
|
| 294 |
+
if (line.startsWith('data: ')) {
|
| 295 |
+
try {
|
| 296 |
+
const data = JSON.parse(line.slice(6));
|
| 297 |
+
|
| 298 |
+
if (data.type === 'start') {
|
| 299 |
+
sessionId = data.session_id;
|
| 300 |
+
if (data.interaction_count) {
|
| 301 |
+
document.getElementById('interactionCount').textContent = data.interaction_count;
|
| 302 |
+
}
|
| 303 |
+
hideTyping();
|
| 304 |
+
assistantMessage = addMessage('', false);
|
| 305 |
+
} else if (data.type === 'token' && assistantMessage) {
|
| 306 |
+
fullResponse += data.content;
|
| 307 |
+
assistantMessage.textContent = fullResponse;
|
| 308 |
+
chatContainer.scrollTop = chatContainer.scrollHeight;
|
| 309 |
+
}
|
| 310 |
+
} catch (e) {
|
| 311 |
+
console.error('Parse error:', e);
|
| 312 |
+
}
|
| 313 |
+
}
|
| 314 |
+
}
|
| 315 |
+
}
|
| 316 |
+
} catch (error) {
|
| 317 |
+
console.error('Error:', error);
|
| 318 |
+
hideTyping();
|
| 319 |
+
addMessage('Sorry, I encountered an error. Please try again.', false);
|
| 320 |
+
} finally {
|
| 321 |
+
hideTyping();
|
| 322 |
+
isProcessing = false;
|
| 323 |
+
sendButton.disabled = false;
|
| 324 |
+
input.focus();
|
| 325 |
+
}
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
// Auto-focus on load
|
| 329 |
+
window.onload = () => {
|
| 330 |
+
document.getElementById('messageInput').focus();
|
| 331 |
+
};
|
| 332 |
+
</script>
|
| 333 |
+
</body>
|
| 334 |
+
</html>
|