Initial commit
This commit is contained in:
commit
712f188f73
|
|
@ -0,0 +1,285 @@
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
#pdm.lock
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/#use-with-ide
|
||||||
|
.pdm.toml
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be added to the global gitignore or merged into this project gitignore. For a PyCharm
|
||||||
|
# project, it is recommended to include the following files:
|
||||||
|
.idea/
|
||||||
|
*.iws
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
|
||||||
|
# Visual Studio Code
|
||||||
|
.vscode/
|
||||||
|
*.code-workspace
|
||||||
|
|
||||||
|
# Local History for Visual Studio Code
|
||||||
|
.history/
|
||||||
|
|
||||||
|
# Built Visual Studio Code Extensions
|
||||||
|
*.vsix
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
|
||||||
|
# Icon must end with two \r
|
||||||
|
Icon
|
||||||
|
|
||||||
|
# Thumbnails
|
||||||
|
._*
|
||||||
|
|
||||||
|
# Files that might appear in the root of a volume
|
||||||
|
.DocumentRevisions-V100
|
||||||
|
.fseventsd
|
||||||
|
.Spotlight-V100
|
||||||
|
.TemporaryItems
|
||||||
|
.Trashes
|
||||||
|
.VolumeIcon.icns
|
||||||
|
.com.apple.timemachine.donotpresent
|
||||||
|
|
||||||
|
# Directories potentially created on remote AFP share
|
||||||
|
.AppleDB
|
||||||
|
.AppleDesktop
|
||||||
|
Network Trash Folder
|
||||||
|
Temporary Items
|
||||||
|
.apdisk
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
Thumbs.db
|
||||||
|
Thumbs.db:encryptable
|
||||||
|
ehthumbs.db
|
||||||
|
ehthumbs_vista.db
|
||||||
|
|
||||||
|
# Dump file
|
||||||
|
*.stackdump
|
||||||
|
|
||||||
|
# Folder config file
|
||||||
|
[Dd]esktop.ini
|
||||||
|
|
||||||
|
# Recycle Bin used on file shares
|
||||||
|
$RECYCLE.BIN/
|
||||||
|
|
||||||
|
# Windows Installer files
|
||||||
|
*.cab
|
||||||
|
*.msi
|
||||||
|
*.msix
|
||||||
|
*.msm
|
||||||
|
*.msp
|
||||||
|
|
||||||
|
# Windows shortcuts
|
||||||
|
*.lnk
|
||||||
|
|
||||||
|
# Linux
|
||||||
|
*~
|
||||||
|
|
||||||
|
# temporary files which can be created if a process still has a handle open of a deleted file
|
||||||
|
.fuse_hidden*
|
||||||
|
|
||||||
|
# KDE directory preferences
|
||||||
|
.directory
|
||||||
|
|
||||||
|
# Linux trash folder which might appear on any partition or disk
|
||||||
|
.Trash-*
|
||||||
|
|
||||||
|
# .nfs files are created when an open file is removed but is still being accessed
|
||||||
|
.nfs*
|
||||||
|
|
||||||
|
# Streamlit
|
||||||
|
.streamlit/
|
||||||
|
|
||||||
|
# ChromaDB data
|
||||||
|
demo-rag-chroma/
|
||||||
|
*.sqlite3
|
||||||
|
|
||||||
|
# Backup files
|
||||||
|
backups/
|
||||||
|
*.backup
|
||||||
|
*.bak
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
temp/
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
.dockerignore
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env.local
|
||||||
|
.env.development
|
||||||
|
.env.test
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
# IDE specific files
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS generated files
|
||||||
|
.DS_Store?
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Project specific
|
||||||
|
input/
|
||||||
|
processed_files.json
|
||||||
|
|
@ -0,0 +1,289 @@
|
||||||
|
# Docker Deployment Guide
|
||||||
|
|
||||||
|
This guide explains how to deploy the Drakkenheim RAG application using Docker Compose with persistent storage.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Docker and Docker Compose installed on your server
|
||||||
|
- OpenAI API key
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
The project follows Python best practices with a `src/` layout:
|
||||||
|
|
||||||
|
```
|
||||||
|
drakkenheim/
|
||||||
|
├── src/
|
||||||
|
│ └── drakkenheim/ # Main package
|
||||||
|
│ ├── __init__.py # Package initialization
|
||||||
|
│ ├── auth.py # Authentication & session management
|
||||||
|
│ ├── llm.py # LLM, embeddings & vector operations
|
||||||
|
│ ├── document.py # Document processing
|
||||||
|
│ ├── storage.py # File persistence
|
||||||
|
│ └── ui.py # User interface components
|
||||||
|
├── app.py # Main application entry point
|
||||||
|
├── generate_password.py # Password generation utility
|
||||||
|
├── docker-compose.yml # Docker orchestration
|
||||||
|
├── Dockerfile # Container definition
|
||||||
|
└── requirements/ # Dependencies
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits of the `src/` layout:**
|
||||||
|
- **Clean separation** between source code and project files
|
||||||
|
- **Prevents import issues** during development and testing
|
||||||
|
- **Industry standard** for Python projects
|
||||||
|
- **Better packaging** and distribution support
|
||||||
|
- **Easier testing** and CI/CD integration
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
1. **Clone and navigate to the project:**
|
||||||
|
```bash
|
||||||
|
git clone <your-repo-url>
|
||||||
|
cd drakkenheim
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Set up environment variables:**
|
||||||
|
```bash
|
||||||
|
# Copy the example environment file
|
||||||
|
cp env.example .env
|
||||||
|
|
||||||
|
# Generate secure password hash
|
||||||
|
python3 generate_password.py
|
||||||
|
|
||||||
|
# Edit .env and add your OpenAI API key and generated auth config
|
||||||
|
nano .env
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Build and start the application:**
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Access the application:**
|
||||||
|
- Open your browser to `http://your-server-ip:8501`
|
||||||
|
- The application will be available and ready to use
|
||||||
|
|
||||||
|
## Persistent Storage
|
||||||
|
|
||||||
|
The application uses Docker volumes to persist data between restarts:
|
||||||
|
|
||||||
|
- **ChromaDB Database**: `chroma_data` volume
|
||||||
|
- **Processed Files Metadata**: `processed_files` volume
|
||||||
|
- **Uploaded Files**: `uploaded_files` volume
|
||||||
|
|
||||||
|
## Management Commands
|
||||||
|
|
||||||
|
### Start the application:
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stop the application:
|
||||||
|
```bash
|
||||||
|
docker compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
### View logs:
|
||||||
|
```bash
|
||||||
|
docker compose logs -f rag-app
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restart the application:
|
||||||
|
```bash
|
||||||
|
docker compose restart rag-app
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update the application:
|
||||||
|
```bash
|
||||||
|
# Pull latest changes
|
||||||
|
git pull
|
||||||
|
|
||||||
|
# Rebuild and restart
|
||||||
|
docker compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backup data:
|
||||||
|
```bash
|
||||||
|
# Create backup directory
|
||||||
|
mkdir -p backups
|
||||||
|
|
||||||
|
# Backup ChromaDB data
|
||||||
|
docker run --rm -v drakkenheim_chroma_data:/data -v $(pwd)/backups:/backup alpine tar czf /backup/chroma_data_$(date +%Y%m%d_%H%M%S).tar.gz -C /data .
|
||||||
|
|
||||||
|
# Backup processed files
|
||||||
|
docker run --rm -v drakkenheim_processed_files:/data -v $(pwd)/backups:/backup alpine tar czf /backup/processed_files_$(date +%Y%m%d_%H%M%S).tar.gz -C /data .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restore data:
|
||||||
|
```bash
|
||||||
|
# Restore ChromaDB data
|
||||||
|
docker run --rm -v drakkenheim_chroma_data:/data -v $(pwd)/backups:/backup alpine tar xzf /backup/chroma_data_YYYYMMDD_HHMMSS.tar.gz -C /data
|
||||||
|
|
||||||
|
# Restore processed files
|
||||||
|
docker run --rm -v drakkenheim_processed_files:/data -v $(pwd)/backups:/backup alpine tar xzf /backup/processed_files_YYYYMMDD_HHMMSS.tar.gz -C /data
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
Edit `.env` file to customize:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Required
|
||||||
|
OPENAI_API_KEY=your_api_key_here
|
||||||
|
|
||||||
|
# Authentication (REQUIRED for production)
|
||||||
|
APP_PASSWORD_HASH=your_hashed_password_here
|
||||||
|
PASSWORD_SALT=your_random_salt_here
|
||||||
|
SESSION_TIMEOUT=3600
|
||||||
|
|
||||||
|
# Optional overrides
|
||||||
|
OPENAI_EMBEDDING_MODEL=text-embedding-3-small
|
||||||
|
OPENAI_LLM_MODEL=gpt-4o-mini
|
||||||
|
STREAMLIT_SERVER_PORT=8501
|
||||||
|
```
|
||||||
|
|
||||||
|
### Authentication Setup
|
||||||
|
|
||||||
|
**Generate secure credentials:**
|
||||||
|
```bash
|
||||||
|
# Run the password generator
|
||||||
|
python3 generate_password.py
|
||||||
|
|
||||||
|
# Follow the prompts to set your password
|
||||||
|
# Copy the generated values to your .env file
|
||||||
|
```
|
||||||
|
|
||||||
|
**Security Features:**
|
||||||
|
- **Password hashing**: SHA-256 with salt
|
||||||
|
- **Session timeout**: Automatic logout after inactivity
|
||||||
|
- **Secure storage**: Passwords never stored in plain text
|
||||||
|
- **Development mode**: No auth required if no password is set
|
||||||
|
|
||||||
|
### Port Configuration
|
||||||
|
|
||||||
|
To change the port, modify `docker-compose.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
ports:
|
||||||
|
- "8080:8501" # Change 8080 to your desired port
|
||||||
|
```
|
||||||
|
|
||||||
|
## Monitoring
|
||||||
|
|
||||||
|
### Health Check
|
||||||
|
The application includes a health check that monitors:
|
||||||
|
- Streamlit server availability
|
||||||
|
- Automatic restart on failure
|
||||||
|
|
||||||
|
### Logs
|
||||||
|
```bash
|
||||||
|
# View all logs
|
||||||
|
docker-compose logs
|
||||||
|
|
||||||
|
# Follow logs in real-time
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# View specific service logs
|
||||||
|
docker-compose logs rag-app
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **Port already in use:**
|
||||||
|
```bash
|
||||||
|
# Check what's using the port
|
||||||
|
sudo netstat -tulpn | grep :8501
|
||||||
|
|
||||||
|
# Kill the process or change the port in docker-compose.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Permission issues:**
|
||||||
|
```bash
|
||||||
|
# Fix volume permissions
|
||||||
|
sudo chown -R 1000:1000 /var/lib/docker/volumes/drakkenheim_*/
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Out of disk space:**
|
||||||
|
```bash
|
||||||
|
# Clean up unused Docker resources
|
||||||
|
docker system prune -a
|
||||||
|
|
||||||
|
# Check volume sizes
|
||||||
|
docker system df -v
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **API key issues:**
|
||||||
|
- Verify your `.env` file has the correct API key
|
||||||
|
- Check the logs for authentication errors
|
||||||
|
- Ensure the API key has sufficient credits
|
||||||
|
|
||||||
|
### Reset Application
|
||||||
|
```bash
|
||||||
|
# Stop and remove containers
|
||||||
|
docker compose down
|
||||||
|
|
||||||
|
# Remove volumes (WARNING: This deletes all data)
|
||||||
|
docker volume rm drakkenheim_chroma_data drakkenheim_processed_files drakkenheim_uploaded_files
|
||||||
|
|
||||||
|
# Start fresh
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
1. **API Key Security:**
|
||||||
|
- Never commit `.env` files to version control
|
||||||
|
- Use Docker secrets for production deployments
|
||||||
|
- Rotate API keys regularly
|
||||||
|
|
||||||
|
2. **Network Security:**
|
||||||
|
- Use a reverse proxy (nginx) for production
|
||||||
|
- Enable HTTPS with SSL certificates
|
||||||
|
- Restrict access with firewall rules
|
||||||
|
|
||||||
|
3. **Data Security:**
|
||||||
|
- Regular backups of persistent volumes
|
||||||
|
- Encrypt sensitive data at rest
|
||||||
|
- Monitor access logs
|
||||||
|
|
||||||
|
## Production Deployment
|
||||||
|
|
||||||
|
For production deployment, consider:
|
||||||
|
|
||||||
|
1. **Reverse Proxy Setup:**
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name your-domain.com;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://localhost:8501;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **SSL Certificate:**
|
||||||
|
```bash
|
||||||
|
# Using Let's Encrypt
|
||||||
|
sudo certbot --nginx -d your-domain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Monitoring:**
|
||||||
|
- Set up log aggregation
|
||||||
|
- Monitor resource usage
|
||||||
|
- Set up alerts for failures
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
If you encounter issues:
|
||||||
|
1. Check the logs: `docker compose logs -f`
|
||||||
|
2. Verify your environment variables
|
||||||
|
3. Ensure Docker has sufficient resources
|
||||||
|
4. Check the troubleshooting section above
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
# Use Python 3.11 slim image
|
||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install system dependencies
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
build-essential \
|
||||||
|
curl \
|
||||||
|
git \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy requirements first for better caching
|
||||||
|
COPY requirements/requirements.txt .
|
||||||
|
|
||||||
|
# Install Python dependencies
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Copy application code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Create directories for persistent storage
|
||||||
|
RUN mkdir -p /app/data/chroma-db
|
||||||
|
RUN mkdir -p /app/data/processed-files
|
||||||
|
RUN mkdir -p /app/data/uploaded-files
|
||||||
|
|
||||||
|
# Add src directory to Python path
|
||||||
|
ENV PYTHONPATH=/app/src:$PYTHONPATH
|
||||||
|
|
||||||
|
# Expose Streamlit port
|
||||||
|
EXPOSE 8501
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health
|
||||||
|
|
||||||
|
# Run the application
|
||||||
|
CMD ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"]
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
SHELL :=/bin/bash
|
||||||
|
|
||||||
|
.PHONY: clean check setup docker-build docker-up docker-down docker-logs docker-restart docker-backup
|
||||||
|
.DEFAULT_GOAL=help
|
||||||
|
VENV_DIR = .venv
|
||||||
|
PYTHON_VERSION = python3
|
||||||
|
|
||||||
|
check: # Ruff check
|
||||||
|
@ruff check .
|
||||||
|
@echo "✅ Check complete!"
|
||||||
|
|
||||||
|
fix: # Fix auto-fixable linting issues
|
||||||
|
@ruff check app.py --fix
|
||||||
|
|
||||||
|
clean: # Clean temporary files
|
||||||
|
@rm -rf __pycache__ .pytest_cache
|
||||||
|
@find . -name '*.pyc' -exec rm -r {} +
|
||||||
|
@find . -name '__pycache__' -exec rm -r {} +
|
||||||
|
@rm -rf build dist
|
||||||
|
@find . -name '*.egg-info' -type d -exec rm -r {} +
|
||||||
|
|
||||||
|
run: # Run the application
|
||||||
|
@PYTHONPATH=src streamlit run app.py
|
||||||
|
|
||||||
|
setup: # Initial project setup
|
||||||
|
@echo "Creating virtual env at: $(VENV_DIR)"
|
||||||
|
@$(PYTHON_VERSION) -m venv $(VENV_DIR)
|
||||||
|
@echo "Installing dependencies..."
|
||||||
|
@source $(VENV_DIR)/bin/activate && pip install -r requirements/requirements-dev.txt && pip install -r requirements/requirements.txt
|
||||||
|
@echo -e "\n✅ Done.\n🎉 Run the following commands to get started:\n\n ➡️ source $(VENV_DIR)/bin/activate\n ➡️ export OPENAI_API_KEY=your_api_key_here\n ➡️ make run\n"
|
||||||
|
|
||||||
|
|
||||||
|
docker-build: # Build Docker image
|
||||||
|
@echo "Building Docker image..."
|
||||||
|
@docker compose build
|
||||||
|
@echo "✅ Docker image built!"
|
||||||
|
|
||||||
|
docker-up: # Start the application with Docker Compose
|
||||||
|
@echo "Starting RAG application with Docker Compose..."
|
||||||
|
@docker compose up -d
|
||||||
|
@echo "✅ Application started! Access at http://localhost:8501"
|
||||||
|
|
||||||
|
docker-down: # Stop the application
|
||||||
|
@echo "Stopping RAG application..."
|
||||||
|
@docker compose down
|
||||||
|
@echo "✅ Application stopped!"
|
||||||
|
|
||||||
|
docker-logs: # View application logs
|
||||||
|
@docker compose logs -f rag-app
|
||||||
|
|
||||||
|
docker-restart: # Restart the application
|
||||||
|
@echo "Restarting RAG application..."
|
||||||
|
@docker compose restart rag-app
|
||||||
|
@echo "✅ Application restarted!"
|
||||||
|
|
||||||
|
docker-backup: # Backup persistent data
|
||||||
|
@echo "Creating backup directory..."
|
||||||
|
@mkdir -p backups
|
||||||
|
@echo "Backing up ChromaDB data..."
|
||||||
|
@docker run --rm -v drakkenheim_chroma_data:/data -v $(PWD)/backups:/backup alpine tar czf /backup/chroma_data_$$(date +%Y%m%d_%H%M%S).tar.gz -C /data .
|
||||||
|
@echo "Backing up processed files..."
|
||||||
|
@docker run --rm -v drakkenheim_processed_files:/data -v $(PWD)/backups:/backup alpine tar czf /backup/processed_files_$$(date +%Y%m%d_%H%M%S).tar.gz -C /data .
|
||||||
|
@echo "✅ Backup completed! Check the backups/ directory"
|
||||||
|
|
||||||
|
docker-clean: # Clean up Docker resources
|
||||||
|
@echo "Cleaning up Docker resources..."
|
||||||
|
@docker compose down
|
||||||
|
@docker system prune -f
|
||||||
|
@echo "✅ Docker cleanup completed!"
|
||||||
|
|
||||||
|
help: # Show this help
|
||||||
|
@egrep -h '\s#\s' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?# "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
"""
|
||||||
|
Drakkenheim RAG Application - Main Entry Point
|
||||||
|
|
||||||
|
A Streamlit-based RAG (Retrieval-Augmented Generation) application that allows users to
|
||||||
|
upload documents and ask questions about their content using OpenAI's GPT models.
|
||||||
|
|
||||||
|
This is the main application file that orchestrates all the modules.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import warnings
|
||||||
|
import streamlit as st
|
||||||
|
|
||||||
|
# Import our custom modules
|
||||||
|
from src.drakkenheim import check_authentication, login_form, load_processed_files, render_sidebar, render_chat_interface
|
||||||
|
|
||||||
|
# Set environment variables to suppress warnings
|
||||||
|
os.environ["TOKENIZERS_PARALLELISM"] = "false"
|
||||||
|
os.environ["CHROMA_TELEMETRY"] = "false"
|
||||||
|
os.environ["TOKENIZERS_VERBOSITY"] = "error"
|
||||||
|
|
||||||
|
# Suppress specific warnings
|
||||||
|
warnings.filterwarnings("ignore", category=UserWarning, module="sentence_transformers")
|
||||||
|
warnings.filterwarnings("ignore", category=UserWarning, module="transformers")
|
||||||
|
warnings.filterwarnings("ignore", category=FutureWarning)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main application function."""
|
||||||
|
# Set page configuration
|
||||||
|
st.set_page_config(
|
||||||
|
page_title="RAG Chat Assistant",
|
||||||
|
page_icon="🤖",
|
||||||
|
layout="wide"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check authentication first
|
||||||
|
if not check_authentication():
|
||||||
|
login_form()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Initialize session state
|
||||||
|
if "messages" not in st.session_state:
|
||||||
|
st.session_state.messages = []
|
||||||
|
|
||||||
|
if "documents_loaded" not in st.session_state:
|
||||||
|
st.session_state.documents_loaded = False
|
||||||
|
|
||||||
|
if "processed_files" not in st.session_state:
|
||||||
|
st.session_state.processed_files = []
|
||||||
|
|
||||||
|
if "files_loaded" not in st.session_state:
|
||||||
|
st.session_state.files_loaded = False
|
||||||
|
load_processed_files()
|
||||||
|
|
||||||
|
# Render the application
|
||||||
|
render_sidebar()
|
||||||
|
render_chat_interface()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Drakkenheim RAG Application Deployment Script
|
||||||
|
# This script helps you deploy the RAG application using Docker Compose
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🚀 Drakkenheim RAG Application Deployment"
|
||||||
|
echo "========================================"
|
||||||
|
|
||||||
|
# Check if Docker is installed
|
||||||
|
if ! command -v docker &> /dev/null; then
|
||||||
|
echo "❌ Docker is not installed. Please install Docker first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if Docker Compose is available
|
||||||
|
if ! docker compose version &> /dev/null; then
|
||||||
|
echo "❌ Docker Compose is not available. Please ensure Docker is up to date."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if .env file exists
|
||||||
|
if [ ! -f .env ]; then
|
||||||
|
echo "📝 Creating .env file from template..."
|
||||||
|
cp env.example .env
|
||||||
|
echo ""
|
||||||
|
echo "🔐 Setting up authentication..."
|
||||||
|
echo "Generating secure password hash..."
|
||||||
|
python3 generate_password.py
|
||||||
|
echo ""
|
||||||
|
echo "⚠️ Please edit .env file and add your OpenAI API key:"
|
||||||
|
echo " nano .env"
|
||||||
|
echo ""
|
||||||
|
read -p "Press Enter after you've added your API key..."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if OPENAI_API_KEY is set
|
||||||
|
if ! grep -q "OPENAI_API_KEY=sk-" .env; then
|
||||||
|
echo "❌ OPENAI_API_KEY not found in .env file."
|
||||||
|
echo "Please edit .env file and add your OpenAI API key:"
|
||||||
|
echo " OPENAI_API_KEY=sk-your-key-here"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if authentication is configured
|
||||||
|
if ! grep -q "APP_PASSWORD_HASH=" .env || grep -q "APP_PASSWORD_HASH=your_hashed_password_here" .env; then
|
||||||
|
echo "⚠️ Authentication not configured. Generating secure password..."
|
||||||
|
python3 generate_password.py
|
||||||
|
echo ""
|
||||||
|
echo "Please copy the generated values to your .env file and restart deployment."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Environment configuration looks good!"
|
||||||
|
|
||||||
|
# Build the Docker image
|
||||||
|
echo "🔨 Building Docker image..."
|
||||||
|
docker compose build
|
||||||
|
|
||||||
|
# Start the application
|
||||||
|
echo "🚀 Starting RAG application..."
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# Wait for the application to start
|
||||||
|
echo "⏳ Waiting for application to start..."
|
||||||
|
sleep 10
|
||||||
|
|
||||||
|
# Check if the application is running
|
||||||
|
if docker compose ps | grep -q "Up"; then
|
||||||
|
echo "✅ Application is running!"
|
||||||
|
echo ""
|
||||||
|
echo "🌐 Access your RAG application at:"
|
||||||
|
echo " http://localhost:8501"
|
||||||
|
echo ""
|
||||||
|
echo "📊 To view logs:"
|
||||||
|
echo " make docker-logs"
|
||||||
|
echo ""
|
||||||
|
echo "🛑 To stop the application:"
|
||||||
|
echo " make docker-down"
|
||||||
|
echo ""
|
||||||
|
echo "📋 To backup your data:"
|
||||||
|
echo " make docker-backup"
|
||||||
|
else
|
||||||
|
echo "❌ Application failed to start. Check logs with:"
|
||||||
|
echo " make docker-logs"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
rag-app:
|
||||||
|
build: .
|
||||||
|
container_name: drakkenheim-rag
|
||||||
|
ports:
|
||||||
|
- "8501:8501"
|
||||||
|
environment:
|
||||||
|
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
||||||
|
- APP_PASSWORD_HASH=${APP_PASSWORD_HASH}
|
||||||
|
- PASSWORD_SALT=${PASSWORD_SALT}
|
||||||
|
- SESSION_TIMEOUT=${SESSION_TIMEOUT:-3600}
|
||||||
|
- TOKENIZERS_PARALLELISM=false
|
||||||
|
- CHROMA_TELEMETRY=false
|
||||||
|
- TOKENIZERS_VERBOSITY=error
|
||||||
|
- CHROMA_DB_PATH=/app/data/chroma-db
|
||||||
|
- PROCESSED_FILES_PATH=/app/data/processed-files
|
||||||
|
volumes:
|
||||||
|
# Persistent storage for ChromaDB database
|
||||||
|
- chroma_data:/app/data/chroma-db
|
||||||
|
# Persistent storage for processed files metadata
|
||||||
|
- processed_files:/app/data/processed-files
|
||||||
|
# Persistent storage for uploaded files (optional)
|
||||||
|
- uploaded_files:/app/data/uploaded-files
|
||||||
|
# Mount the input directory for initial documents
|
||||||
|
- ./input:/app/input:ro
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:8501/_stcore/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
chroma_data:
|
||||||
|
driver: local
|
||||||
|
processed_files:
|
||||||
|
driver: local
|
||||||
|
uploaded_files:
|
||||||
|
driver: local
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
# OpenAI API Configuration
|
||||||
|
OPENAI_API_KEY=your_openai_api_key_here
|
||||||
|
|
||||||
|
# Authentication Configuration (REQUIRED for production)
|
||||||
|
APP_PASSWORD_HASH=your_hashed_password_here
|
||||||
|
PASSWORD_SALT=your_random_salt_here
|
||||||
|
SESSION_TIMEOUT=3600
|
||||||
|
|
||||||
|
# Optional: Override default models
|
||||||
|
# OPENAI_EMBEDDING_MODEL=text-embedding-3-small
|
||||||
|
# OPENAI_LLM_MODEL=gpt-4o-mini
|
||||||
|
|
||||||
|
# Optional: Streamlit configuration
|
||||||
|
# STREAMLIT_SERVER_PORT=8501
|
||||||
|
# STREAMLIT_SERVER_ADDRESS=0.0.0.0
|
||||||
|
|
||||||
|
# Optional: ChromaDB configuration
|
||||||
|
# CHROMA_DB_PATH=/app/data/chroma-db
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Password Generator for Drakkenheim RAG Application
|
||||||
|
|
||||||
|
This script helps you generate secure password hashes and salts for authentication.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import secrets
|
||||||
|
import sys
|
||||||
|
import getpass
|
||||||
|
|
||||||
|
def generate_salt(length=32):
|
||||||
|
"""Generate a random salt."""
|
||||||
|
return secrets.token_hex(length)
|
||||||
|
|
||||||
|
def hash_password(password, salt):
|
||||||
|
"""Hash a password with salt using SHA-256."""
|
||||||
|
return hashlib.sha256((password + salt).encode()).hexdigest()
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("🔐 Drakkenheim RAG - Password Generator")
|
||||||
|
print("=" * 40)
|
||||||
|
|
||||||
|
# Get password from user
|
||||||
|
password = getpass.getpass("Enter password: ")
|
||||||
|
if not password:
|
||||||
|
print("❌ Password cannot be empty!")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Confirm password
|
||||||
|
confirm_password = getpass.getpass("Confirm password: ")
|
||||||
|
if password != confirm_password:
|
||||||
|
print("❌ Passwords don't match!")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Generate salt and hash
|
||||||
|
salt = generate_salt()
|
||||||
|
password_hash = hash_password(password, salt)
|
||||||
|
|
||||||
|
print("\n✅ Password configuration generated!")
|
||||||
|
print("\n📋 Add these to your .env file:")
|
||||||
|
print("-" * 40)
|
||||||
|
print(f"APP_PASSWORD_HASH={password_hash}")
|
||||||
|
print(f"PASSWORD_SALT={salt}")
|
||||||
|
print("SESSION_TIMEOUT=3600")
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
print("\n🔒 Security notes:")
|
||||||
|
print("• Keep your .env file secure and never commit it to version control")
|
||||||
|
print("• The salt should be unique for each deployment")
|
||||||
|
print("• SESSION_TIMEOUT is in seconds (3600 = 1 hour)")
|
||||||
|
print("• Users will be logged out automatically after the timeout")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
ruff==0.7.4
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
openai==1.3.0 # OpenAI API (compatible with ChromaDB 0.4.22)
|
||||||
|
chromadb==0.4.22 # Vector Database (compatible with OpenAI 1.12.0)
|
||||||
|
sentence-transformers==3.3.1 # CrossEncoder Re-ranking
|
||||||
|
streamlit==1.40.1 # Application UI
|
||||||
|
PyMuPDF==1.24.14 # PDF Document loader
|
||||||
|
langchain-community==0.3.7 # Utils for text splitting
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
"""
|
||||||
|
Drakkenheim RAG Application
|
||||||
|
|
||||||
|
A Streamlit-based RAG (Retrieval-Augmented Generation) application that allows users to
|
||||||
|
upload documents and ask questions about their content using OpenAI's GPT models.
|
||||||
|
|
||||||
|
This package contains all the core modules for the application.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__version__ = "1.0.0"
|
||||||
|
__author__ = "Drakkenheim Team"
|
||||||
|
__description__ = "RAG Chat Assistant with Document Processing"
|
||||||
|
|
||||||
|
# Import main components for easy access
|
||||||
|
from .auth import check_authentication, login_form, logout
|
||||||
|
from .llm import get_vector_collection, add_to_vector_collection, query_collection, call_llm
|
||||||
|
from .document import process_document, normalize_uploaded_file_name
|
||||||
|
from .storage import save_processed_files, load_processed_files
|
||||||
|
from .ui import render_sidebar, render_chat_interface
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"check_authentication",
|
||||||
|
"login_form",
|
||||||
|
"logout",
|
||||||
|
"get_vector_collection",
|
||||||
|
"add_to_vector_collection",
|
||||||
|
"query_collection",
|
||||||
|
"call_llm",
|
||||||
|
"process_document",
|
||||||
|
"normalize_uploaded_file_name",
|
||||||
|
"save_processed_files",
|
||||||
|
"load_processed_files",
|
||||||
|
"render_sidebar",
|
||||||
|
"render_chat_interface",
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
"""
|
||||||
|
Authentication module for Drakkenheim RAG Application.
|
||||||
|
|
||||||
|
Handles user authentication, password verification, and session management.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import hashlib
|
||||||
|
from datetime import datetime
|
||||||
|
import streamlit as st
|
||||||
|
|
||||||
|
# Authentication configuration
|
||||||
|
AUTH_CONFIG = {
|
||||||
|
"password_hash": os.getenv("APP_PASSWORD_HASH", ""),
|
||||||
|
"session_timeout": int(os.getenv("SESSION_TIMEOUT", "3600")), # 1 hour default
|
||||||
|
}
|
||||||
|
|
||||||
|
def hash_password(password: str) -> str:
|
||||||
|
"""Hash a password using SHA-256 with salt."""
|
||||||
|
salt = os.getenv("PASSWORD_SALT", "default_salt_change_in_production")
|
||||||
|
return hashlib.sha256((password + salt).encode()).hexdigest()
|
||||||
|
|
||||||
|
def verify_password(password: str) -> bool:
|
||||||
|
"""Verify if the provided password is correct."""
|
||||||
|
if not AUTH_CONFIG["password_hash"]:
|
||||||
|
# If no password is set, allow access (for development)
|
||||||
|
return True
|
||||||
|
|
||||||
|
password_hash = hash_password(password)
|
||||||
|
return password_hash == AUTH_CONFIG["password_hash"]
|
||||||
|
|
||||||
|
def check_authentication():
|
||||||
|
"""Check if user is authenticated and session is valid."""
|
||||||
|
if "authenticated" not in st.session_state:
|
||||||
|
st.session_state.authenticated = False
|
||||||
|
st.session_state.login_time = None
|
||||||
|
|
||||||
|
if st.session_state.authenticated:
|
||||||
|
# Check session timeout
|
||||||
|
if st.session_state.login_time:
|
||||||
|
current_time = datetime.now().timestamp()
|
||||||
|
if current_time - st.session_state.login_time > AUTH_CONFIG["session_timeout"]:
|
||||||
|
st.session_state.authenticated = False
|
||||||
|
st.session_state.login_time = None
|
||||||
|
st.rerun()
|
||||||
|
|
||||||
|
return st.session_state.authenticated
|
||||||
|
|
||||||
|
def login_form():
|
||||||
|
"""Display login form."""
|
||||||
|
st.title("🔐 Drakkenheim RAG - Login")
|
||||||
|
st.markdown("Please enter the password to access the application.")
|
||||||
|
|
||||||
|
with st.form("login_form"):
|
||||||
|
password = st.text_input("Password", type="password", placeholder="Enter password")
|
||||||
|
submit_button = st.form_submit_button("Login", type="primary")
|
||||||
|
|
||||||
|
if submit_button:
|
||||||
|
if verify_password(password):
|
||||||
|
st.session_state.authenticated = True
|
||||||
|
st.session_state.login_time = datetime.now().timestamp()
|
||||||
|
st.success("✅ Login successful!")
|
||||||
|
st.rerun()
|
||||||
|
else:
|
||||||
|
st.error("❌ Invalid password. Please try again.")
|
||||||
|
|
||||||
|
# Show help for setting up password
|
||||||
|
with st.expander("🔧 Setup Instructions"):
|
||||||
|
st.markdown("""
|
||||||
|
**To set up authentication:**
|
||||||
|
|
||||||
|
1. Set the `APP_PASSWORD_HASH` environment variable with a hashed password
|
||||||
|
2. Set the `PASSWORD_SALT` environment variable for additional security
|
||||||
|
3. Restart the application
|
||||||
|
|
||||||
|
**Generate password hash:**
|
||||||
|
```bash
|
||||||
|
python3 -c "import hashlib; print(hashlib.sha256(('your_password' + 'your_salt').encode()).hexdigest())"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example .env configuration:**
|
||||||
|
```
|
||||||
|
APP_PASSWORD_HASH=your_hashed_password_here
|
||||||
|
PASSWORD_SALT=your_random_salt_here
|
||||||
|
SESSION_TIMEOUT=3600
|
||||||
|
```
|
||||||
|
""")
|
||||||
|
|
||||||
|
def logout():
|
||||||
|
"""Logout the current user."""
|
||||||
|
st.session_state.authenticated = False
|
||||||
|
st.session_state.login_time = None
|
||||||
|
st.rerun()
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
"""
|
||||||
|
Document Processing module for Drakkenheim RAG Application.
|
||||||
|
|
||||||
|
Handles document loading, processing, and text splitting for various file formats.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import json
|
||||||
|
from langchain_community.document_loaders import PyMuPDFLoader
|
||||||
|
from langchain_core.documents import Document
|
||||||
|
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
||||||
|
from streamlit.runtime.uploaded_file_manager import UploadedFile
|
||||||
|
|
||||||
|
def process_document(uploaded_file: UploadedFile) -> list[Document]:
|
||||||
|
"""Process uploaded document and split into chunks for vector storage.
|
||||||
|
|
||||||
|
Handles multiple file formats (PDF, TXT, JSON, MD) and converts them into
|
||||||
|
Document objects that can be processed by the vector database.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
uploaded_file: Streamlit UploadedFile object containing the document
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[Document]: List of Document objects ready for vector storage
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If file type is not supported or file is invalid
|
||||||
|
"""
|
||||||
|
file_extension = uploaded_file.name.split('.')[-1].lower()
|
||||||
|
|
||||||
|
if file_extension == 'pdf':
|
||||||
|
# Save uploaded file temporarily
|
||||||
|
with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp_file:
|
||||||
|
tmp_file.write(uploaded_file.getvalue())
|
||||||
|
tmp_file_path = tmp_file.name
|
||||||
|
|
||||||
|
# Load PDF using PyMuPDF
|
||||||
|
loader = PyMuPDFLoader(tmp_file_path)
|
||||||
|
docs = loader.load()
|
||||||
|
|
||||||
|
# Clean up temporary file
|
||||||
|
os.unlink(tmp_file_path)
|
||||||
|
|
||||||
|
elif file_extension in ['txt', 'text']:
|
||||||
|
content = uploaded_file.read().decode('utf-8')
|
||||||
|
docs = [Document(page_content=content, metadata={"source": uploaded_file.name})]
|
||||||
|
|
||||||
|
elif file_extension == 'json':
|
||||||
|
content = uploaded_file.read().decode('utf-8')
|
||||||
|
try:
|
||||||
|
json_data = json.loads(content)
|
||||||
|
formatted_content = json.dumps(json_data, indent=2, ensure_ascii=False)
|
||||||
|
docs = [Document(page_content=formatted_content, metadata={"source": uploaded_file.name, "type": "json"})]
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
raise ValueError(f"Invalid JSON file: {str(e)}")
|
||||||
|
|
||||||
|
elif file_extension in ['md', 'markdown']:
|
||||||
|
content = uploaded_file.read().decode('utf-8')
|
||||||
|
docs = [Document(page_content=content, metadata={"source": uploaded_file.name, "type": "markdown"})]
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unsupported file type: {file_extension}. Supported types: pdf, txt, json, md")
|
||||||
|
|
||||||
|
# Split documents into chunks
|
||||||
|
text_splitter = RecursiveCharacterTextSplitter(
|
||||||
|
chunk_size=200, # Smaller chunks to avoid token limits
|
||||||
|
chunk_overlap=50,
|
||||||
|
separators=["\n\n", "\n", ".", "?", "!", " ", ""],
|
||||||
|
)
|
||||||
|
return text_splitter.split_documents(docs)
|
||||||
|
|
||||||
|
def normalize_uploaded_file_name(uploaded_file: UploadedFile) -> str:
|
||||||
|
"""Normalize uploaded file name for consistent storage.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
uploaded_file: Streamlit UploadedFile object
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Normalized file name safe for use as identifier
|
||||||
|
"""
|
||||||
|
return uploaded_file.name.replace(" ", "_").replace("/", "_").replace("\\", "_")
|
||||||
|
|
@ -0,0 +1,197 @@
|
||||||
|
"""
|
||||||
|
LLM and Vector Operations module for Drakkenheim RAG Application.
|
||||||
|
|
||||||
|
Handles OpenAI API interactions, embeddings, vector operations, and document re-ranking.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import openai
|
||||||
|
import chromadb
|
||||||
|
from sentence_transformers import CrossEncoder
|
||||||
|
|
||||||
|
# Custom OpenAI embedding function to avoid ChromaDB compatibility issues
|
||||||
|
class CustomOpenAIEmbeddingFunction:
|
||||||
|
def __init__(self, api_key: str, model_name: str = "text-embedding-3-small"):
|
||||||
|
self.api_key = api_key
|
||||||
|
self.model_name = model_name
|
||||||
|
self.client = openai.OpenAI(api_key=api_key)
|
||||||
|
|
||||||
|
def __call__(self, input):
|
||||||
|
"""Generate embeddings for input texts."""
|
||||||
|
try:
|
||||||
|
# Truncate very long texts to avoid token limits
|
||||||
|
max_chars = 8000 # Conservative limit for text-embedding-3-small
|
||||||
|
truncated_input = []
|
||||||
|
for text in input:
|
||||||
|
if len(text) > max_chars:
|
||||||
|
truncated_input.append(text[:max_chars])
|
||||||
|
print(f"Warning: Truncated text from {len(text)} to {max_chars} characters")
|
||||||
|
else:
|
||||||
|
truncated_input.append(text)
|
||||||
|
|
||||||
|
response = self.client.embeddings.create(
|
||||||
|
model=self.model_name,
|
||||||
|
input=truncated_input
|
||||||
|
)
|
||||||
|
return [embedding.embedding for embedding in response.data]
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error generating embeddings: {e}")
|
||||||
|
return [[0.0] * 1536 for _ in input] # Return zero vectors as fallback
|
||||||
|
|
||||||
|
def get_vector_collection():
|
||||||
|
"""Gets or creates a ChromaDB collection for vector storage.
|
||||||
|
|
||||||
|
Creates a custom OpenAI embedding function using the text-embedding-3-small model and initializes
|
||||||
|
a persistent ChromaDB client. Returns a collection that can be used to store and
|
||||||
|
query document embeddings.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
chromadb.Collection: A ChromaDB collection configured with the custom OpenAI embedding
|
||||||
|
function and cosine similarity space.
|
||||||
|
"""
|
||||||
|
openai_ef = CustomOpenAIEmbeddingFunction(
|
||||||
|
api_key=os.getenv("OPENAI_API_KEY"),
|
||||||
|
model_name="text-embedding-3-small",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Use Docker volume path if available, fallback to local path
|
||||||
|
chroma_path = os.getenv("CHROMA_DB_PATH", "./demo-rag-chroma")
|
||||||
|
chroma_client = chromadb.PersistentClient(path=chroma_path)
|
||||||
|
return chroma_client.get_or_create_collection(
|
||||||
|
name="rag_app",
|
||||||
|
embedding_function=openai_ef,
|
||||||
|
metadata={"hnsw:space": "cosine"},
|
||||||
|
)
|
||||||
|
|
||||||
|
def add_to_vector_collection(all_splits, file_name: str):
|
||||||
|
"""Adds document splits to a vector collection for semantic search.
|
||||||
|
|
||||||
|
Processes document splits in batches to avoid token limits and adds them to the
|
||||||
|
ChromaDB collection with proper metadata and unique IDs.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
all_splits: List of Document objects to add to the collection
|
||||||
|
file_name: Name of the file being processed (used for ID generation)
|
||||||
|
"""
|
||||||
|
collection = get_vector_collection()
|
||||||
|
batch_size = 50 # Process documents in smaller batches
|
||||||
|
|
||||||
|
for batch_start in range(0, len(all_splits), batch_size):
|
||||||
|
batch_end = min(batch_start + batch_size, len(all_splits))
|
||||||
|
batch_splits = all_splits[batch_start:batch_end]
|
||||||
|
|
||||||
|
documents, metadatas, ids = [], [], []
|
||||||
|
|
||||||
|
for idx, split in enumerate(batch_splits):
|
||||||
|
documents.append(split.page_content)
|
||||||
|
metadatas.append(split.metadata)
|
||||||
|
ids.append(f"{file_name}_{batch_start + idx}")
|
||||||
|
|
||||||
|
collection.upsert(
|
||||||
|
documents=documents,
|
||||||
|
metadatas=metadatas,
|
||||||
|
ids=ids,
|
||||||
|
)
|
||||||
|
|
||||||
|
def query_collection(prompt: str, n_results: int = 10):
|
||||||
|
"""Queries the vector collection for relevant documents.
|
||||||
|
|
||||||
|
Searches the ChromaDB collection for documents similar to the given prompt
|
||||||
|
and returns the most relevant results.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
prompt: The search query string
|
||||||
|
n_results: Number of results to return (default: 10)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Query results containing documents, metadatas, distances, and ids
|
||||||
|
"""
|
||||||
|
collection = get_vector_collection()
|
||||||
|
results = collection.query(query_texts=[prompt], n_results=n_results)
|
||||||
|
return results
|
||||||
|
|
||||||
|
def call_llm(context: str, prompt: str):
|
||||||
|
"""Calls the language model with context and prompt to generate a response.
|
||||||
|
|
||||||
|
Uses OpenAI GPT-4o-mini API to stream responses from a language model by providing context and a
|
||||||
|
question prompt. The model uses a system prompt to format and ground its responses appropriately.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
context: String containing the relevant context for answering the question
|
||||||
|
prompt: String containing the user's question
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
String chunks of the generated response as they become available from the model
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
OpenAIError: If there are issues communicating with the OpenAI API
|
||||||
|
"""
|
||||||
|
client = openai.OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
|
||||||
|
|
||||||
|
response = client.chat.completions.create(
|
||||||
|
model="gpt-4o-mini",
|
||||||
|
stream=True,
|
||||||
|
messages=[
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
"content": f"""You are an AI assistant tasked with providing detailed answers based solely on the given context. Your goal is to analyze the information provided and formulate a comprehensive, well-structured response to the question.
|
||||||
|
|
||||||
|
context will be passed as "Context:"
|
||||||
|
user question will be passed as "Question:"
|
||||||
|
|
||||||
|
Instructions:
|
||||||
|
1. Read the context carefully and identify the most relevant information
|
||||||
|
2. Use only the information provided in the context to answer the question
|
||||||
|
3. If the context doesn't contain enough information to answer the question, say so
|
||||||
|
4. Structure your response in a clear and organized manner
|
||||||
|
5. Cite specific parts of the context when relevant
|
||||||
|
6. Be concise but comprehensive
|
||||||
|
|
||||||
|
Context: {context}""",
|
||||||
|
},
|
||||||
|
{"role": "user", "content": prompt},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
for chunk in response:
|
||||||
|
if chunk.choices[0].delta.content is not None:
|
||||||
|
yield chunk.choices[0].delta.content
|
||||||
|
|
||||||
|
def re_rank_cross_encoders(documents: list[str], prompt: str) -> tuple[str, list[int]]:
|
||||||
|
"""Re-ranks documents using a cross-encoder model for more accurate relevance scoring.
|
||||||
|
|
||||||
|
Uses the MS MARCO MiniLM cross-encoder model to re-rank the input documents based on
|
||||||
|
their relevance to the given prompt. This helps improve the quality of retrieved
|
||||||
|
documents for RAG applications.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
documents: List of document texts to re-rank
|
||||||
|
prompt: The query prompt to rank documents against
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: A tuple containing:
|
||||||
|
- The most relevant document text
|
||||||
|
- List of document indices in order of relevance
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: If cross-encoder model fails to load or rank documents
|
||||||
|
"""
|
||||||
|
if not documents:
|
||||||
|
return "", []
|
||||||
|
|
||||||
|
try:
|
||||||
|
encoder_model = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")
|
||||||
|
ranks = encoder_model.rank(prompt, documents, top_k=3)
|
||||||
|
relevant_text_ids = []
|
||||||
|
relevant_text = ""
|
||||||
|
|
||||||
|
for rank in ranks:
|
||||||
|
relevant_text_ids.append(rank["corpus_id"])
|
||||||
|
if not relevant_text:
|
||||||
|
relevant_text = documents[rank["corpus_id"]]
|
||||||
|
|
||||||
|
return relevant_text, relevant_text_ids
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error in cross-encoder ranking: {e}")
|
||||||
|
# Fallback to returning first document
|
||||||
|
return documents[0] if documents else "", [0]
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
"""
|
||||||
|
Storage module for Drakkenheim RAG Application.
|
||||||
|
|
||||||
|
Handles persistent storage of processed files metadata and session state.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
import streamlit as st
|
||||||
|
|
||||||
|
def save_processed_files():
|
||||||
|
"""Save processed files metadata to persistent storage."""
|
||||||
|
try:
|
||||||
|
files_data = {
|
||||||
|
"processed_files": st.session_state.processed_files,
|
||||||
|
"documents_loaded": st.session_state.documents_loaded,
|
||||||
|
"timestamp": datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create metadata directory if it doesn't exist
|
||||||
|
metadata_path = os.getenv("PROCESSED_FILES_PATH", "./demo-rag-chroma/metadata")
|
||||||
|
os.makedirs(metadata_path, exist_ok=True)
|
||||||
|
|
||||||
|
with open(os.path.join(metadata_path, "processed_files.json"), "w") as f:
|
||||||
|
json.dump(files_data, f, indent=2)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error saving processed files: {e}")
|
||||||
|
|
||||||
|
def load_processed_files():
|
||||||
|
"""Load processed files metadata from persistent storage."""
|
||||||
|
try:
|
||||||
|
metadata_path = os.getenv("PROCESSED_FILES_PATH", "./demo-rag-chroma/metadata")
|
||||||
|
metadata_file = os.path.join(metadata_path, "processed_files.json")
|
||||||
|
if os.path.exists(metadata_file):
|
||||||
|
with open(metadata_file, "r") as f:
|
||||||
|
files_data = json.load(f)
|
||||||
|
|
||||||
|
st.session_state.processed_files = files_data.get("processed_files", [])
|
||||||
|
st.session_state.documents_loaded = files_data.get("documents_loaded", False)
|
||||||
|
st.session_state.files_loaded = True
|
||||||
|
|
||||||
|
# Check if the vector collection actually exists
|
||||||
|
try:
|
||||||
|
from .llm import get_vector_collection
|
||||||
|
collection = get_vector_collection()
|
||||||
|
if collection.count() == 0:
|
||||||
|
# Collection is empty, reset the state
|
||||||
|
st.session_state.documents_loaded = False
|
||||||
|
st.session_state.processed_files = []
|
||||||
|
except Exception:
|
||||||
|
# Collection doesn't exist or is corrupted, reset the state
|
||||||
|
st.session_state.documents_loaded = False
|
||||||
|
st.session_state.processed_files = []
|
||||||
|
else:
|
||||||
|
st.session_state.files_loaded = True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading processed files: {e}")
|
||||||
|
st.session_state.files_loaded = True
|
||||||
|
|
@ -0,0 +1,210 @@
|
||||||
|
"""
|
||||||
|
UI Components module for Drakkenheim RAG Application.
|
||||||
|
|
||||||
|
Handles the main user interface components and layout.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import streamlit as st
|
||||||
|
from datetime import datetime
|
||||||
|
from .auth import logout
|
||||||
|
from .llm import query_collection, call_llm, re_rank_cross_encoders, add_to_vector_collection
|
||||||
|
from .document import process_document, normalize_uploaded_file_name
|
||||||
|
from .storage import save_processed_files
|
||||||
|
|
||||||
|
def render_sidebar():
|
||||||
|
"""Render the sidebar with document upload and file management."""
|
||||||
|
with st.sidebar:
|
||||||
|
# Logout button at the top
|
||||||
|
col1, col2 = st.columns([3, 1])
|
||||||
|
with col1:
|
||||||
|
st.title("📚 Document Upload")
|
||||||
|
with col2:
|
||||||
|
if st.button("🚪 Logout", help="Logout from the application"):
|
||||||
|
logout()
|
||||||
|
|
||||||
|
st.divider()
|
||||||
|
|
||||||
|
uploaded_files = st.file_uploader(
|
||||||
|
"Upload your documents",
|
||||||
|
type=["pdf", "txt", "json", "md"],
|
||||||
|
accept_multiple_files=True,
|
||||||
|
help="Supported formats: PDF, Text, JSON, Markdown. You can select multiple files at once!"
|
||||||
|
)
|
||||||
|
|
||||||
|
if st.button("📥 Process Documents", type="primary"):
|
||||||
|
if uploaded_files:
|
||||||
|
try:
|
||||||
|
# Create progress elements
|
||||||
|
progress_bar = st.progress(0)
|
||||||
|
status_text = st.empty()
|
||||||
|
file_progress_text = st.empty()
|
||||||
|
|
||||||
|
total_files = len(uploaded_files)
|
||||||
|
processed_count = 0
|
||||||
|
total_chunks_processed = 0
|
||||||
|
|
||||||
|
# Estimate total chunks for better progress tracking
|
||||||
|
total_chunks_estimated = 0
|
||||||
|
for uploaded_file in uploaded_files:
|
||||||
|
try:
|
||||||
|
# Quick estimation of chunks (rough estimate)
|
||||||
|
content_length = len(uploaded_file.getvalue())
|
||||||
|
estimated_chunks = max(1, content_length // 200) # Rough estimate based on 200 char chunks
|
||||||
|
total_chunks_estimated += estimated_chunks
|
||||||
|
except Exception:
|
||||||
|
total_chunks_estimated += 10 # Fallback estimate
|
||||||
|
|
||||||
|
for i, uploaded_file in enumerate(uploaded_files):
|
||||||
|
# File-level progress
|
||||||
|
file_progress_text.text(f"📁 File {i+1}/{total_files}: {uploaded_file.name}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Process document
|
||||||
|
all_splits = process_document(uploaded_file)
|
||||||
|
actual_chunks = len(all_splits)
|
||||||
|
|
||||||
|
# Add to vector collection with chunk-level progress
|
||||||
|
add_to_vector_collection(all_splits, normalize_uploaded_file_name(uploaded_file))
|
||||||
|
st.session_state.documents_loaded = True
|
||||||
|
|
||||||
|
# Add file to processed files list
|
||||||
|
current_time = datetime.now().strftime("%H:%M:%S")
|
||||||
|
file_info = {
|
||||||
|
"name": uploaded_file.name,
|
||||||
|
"size": f"{len(uploaded_file.getvalue()) / 1024:.1f} KB",
|
||||||
|
"chunks": actual_chunks,
|
||||||
|
"timestamp": current_time
|
||||||
|
}
|
||||||
|
st.session_state.processed_files.append(file_info)
|
||||||
|
save_processed_files()
|
||||||
|
|
||||||
|
# Update status with chunk info
|
||||||
|
status_text.text(f"✅ {uploaded_file.name} - {actual_chunks} chunks processed")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
st.error(f"❌ Error processing {uploaded_file.name}: {str(e)}")
|
||||||
|
|
||||||
|
# Final status
|
||||||
|
progress_bar.progress(1.0)
|
||||||
|
if processed_count == total_files:
|
||||||
|
status_text.text(f"🎉 All {processed_count} files processed! ({total_chunks_processed} total chunks)")
|
||||||
|
file_progress_text.text("✅ Processing complete!")
|
||||||
|
st.success(f"🎉 Successfully processed {processed_count} file(s) with {total_chunks_processed} total chunks!")
|
||||||
|
else:
|
||||||
|
status_text.text(f"⚠️ Processed {processed_count}/{total_files} files ({total_chunks_processed} chunks)")
|
||||||
|
file_progress_text.text("⚠️ Partial completion")
|
||||||
|
st.warning(f"Processed {processed_count} out of {total_files} files successfully")
|
||||||
|
|
||||||
|
# Clear progress after 3 seconds
|
||||||
|
import time
|
||||||
|
time.sleep(3)
|
||||||
|
progress_bar.empty()
|
||||||
|
status_text.empty()
|
||||||
|
file_progress_text.empty()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
st.error(f"Error processing files: {str(e)}")
|
||||||
|
else:
|
||||||
|
st.warning("Please select at least one file to process.")
|
||||||
|
|
||||||
|
# Clear buttons
|
||||||
|
col1, col2 = st.columns(2)
|
||||||
|
with col1:
|
||||||
|
if st.button("🗑️ Clear Chat"):
|
||||||
|
st.session_state.messages = []
|
||||||
|
st.rerun()
|
||||||
|
|
||||||
|
with col2:
|
||||||
|
if st.button("🗑️ Clear All Files"):
|
||||||
|
st.session_state.processed_files = []
|
||||||
|
st.session_state.documents_loaded = False
|
||||||
|
st.session_state.messages = []
|
||||||
|
save_processed_files()
|
||||||
|
st.rerun()
|
||||||
|
|
||||||
|
# Processed Files Overview
|
||||||
|
st.divider()
|
||||||
|
st.subheader("📋 Processed Files")
|
||||||
|
|
||||||
|
if st.session_state.processed_files:
|
||||||
|
# Show total files and chunks
|
||||||
|
total_chunks = sum(file_info['chunks'] for file_info in st.session_state.processed_files)
|
||||||
|
st.metric("Total Files", len(st.session_state.processed_files))
|
||||||
|
st.metric("Total Chunks", total_chunks)
|
||||||
|
|
||||||
|
# List individual files
|
||||||
|
for i, file_info in enumerate(st.session_state.processed_files):
|
||||||
|
with st.expander(f"📄 {file_info['name']}"):
|
||||||
|
col1, col2 = st.columns(2)
|
||||||
|
with col1:
|
||||||
|
st.write(f"**Size:** {file_info['size']}")
|
||||||
|
st.write(f"**Chunks:** {file_info['chunks']}")
|
||||||
|
with col2:
|
||||||
|
st.write(f"**Processed:** {file_info['timestamp']}")
|
||||||
|
|
||||||
|
# Remove file button
|
||||||
|
if st.button("🗑️ Remove", key=f"remove_{i}"):
|
||||||
|
st.session_state.processed_files.pop(i)
|
||||||
|
if not st.session_state.processed_files:
|
||||||
|
st.session_state.documents_loaded = False
|
||||||
|
save_processed_files()
|
||||||
|
st.rerun()
|
||||||
|
else:
|
||||||
|
st.info("No files processed yet")
|
||||||
|
|
||||||
|
# Document status
|
||||||
|
st.divider()
|
||||||
|
if st.session_state.documents_loaded:
|
||||||
|
st.success(f"📄 {len(st.session_state.processed_files)} file(s) loaded and ready for questions!")
|
||||||
|
else:
|
||||||
|
st.info("📄 Upload a document to start chatting")
|
||||||
|
|
||||||
|
def render_chat_interface():
|
||||||
|
"""Render the main chat interface."""
|
||||||
|
# Display chat messages
|
||||||
|
for message in st.session_state.messages:
|
||||||
|
with st.chat_message(message["role"]):
|
||||||
|
st.markdown(message["content"])
|
||||||
|
|
||||||
|
# Chat input
|
||||||
|
if prompt := st.chat_input("Ask a question about your documents..."):
|
||||||
|
# Add user message to chat history
|
||||||
|
st.session_state.messages.append({"role": "user", "content": prompt})
|
||||||
|
|
||||||
|
# Display user message
|
||||||
|
with st.chat_message("user"):
|
||||||
|
st.markdown(prompt)
|
||||||
|
|
||||||
|
# Generate and display assistant response
|
||||||
|
with st.chat_message("assistant"):
|
||||||
|
try:
|
||||||
|
# Query the vector collection
|
||||||
|
results = query_collection(prompt)
|
||||||
|
relevant_text = results.get("documents", [[]])[0]
|
||||||
|
relevant_text_ids = list(range(len(relevant_text)))
|
||||||
|
|
||||||
|
# Re-rank results for better relevance
|
||||||
|
if relevant_text:
|
||||||
|
relevant_text, relevant_text_ids = re_rank_cross_encoders(relevant_text, prompt)
|
||||||
|
|
||||||
|
# Prepare context for LLM
|
||||||
|
context = "\n\n".join(relevant_text) if relevant_text else "No relevant context found."
|
||||||
|
|
||||||
|
# Generate response with streaming
|
||||||
|
message_placeholder = st.empty()
|
||||||
|
full_response = ""
|
||||||
|
|
||||||
|
for chunk in call_llm(context, prompt):
|
||||||
|
full_response += chunk
|
||||||
|
message_placeholder.markdown(full_response + "▌")
|
||||||
|
|
||||||
|
# Remove the cursor and display final response
|
||||||
|
message_placeholder.markdown(full_response)
|
||||||
|
|
||||||
|
# Add assistant response to chat history
|
||||||
|
st.session_state.messages.append({"role": "assistant", "content": full_response})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_message = f"Sorry, I encountered an error: {str(e)}"
|
||||||
|
st.error(error_message)
|
||||||
|
st.session_state.messages.append({"role": "assistant", "content": error_message})
|
||||||
Loading…
Reference in New Issue