Spaces:
Sleeping
Sleeping
final try 1
Browse files- .gitattributes +0 -35
- .gitignore +6 -0
- Dockerfile +40 -12
- LICENSE +21 -0
- README.md +74 -12
- app.py +0 -114
- app/__init__.py +0 -0
- app/api.py +295 -0
- app/backend/__init__.py +0 -0
- app/backend/controllers/__init__.py +0 -0
- app/backend/controllers/base_controller.py +4 -0
- app/backend/controllers/chats.py +5 -0
- app/backend/controllers/schemas.py +32 -0
- app/backend/controllers/users.py +145 -0
- app/backend/models/__init__.py +0 -0
- app/backend/models/base_model.py +9 -0
- app/backend/models/chats.py +25 -0
- app/backend/models/db_service.py +27 -0
- app/backend/models/messages.py +16 -0
- app/backend/models/users.py +47 -0
- app/chunks.py +47 -0
- app/database.py +146 -0
- app/document_validator.py +7 -0
- app/frontend/static/styles.css +206 -0
- app/frontend/templates/base.html +32 -0
- app/frontend/templates/components/navbar.html +8 -0
- app/frontend/templates/pages/chat.html +173 -0
- app/frontend/templates/pages/login.html +79 -0
- app/frontend/templates/pages/main.html +9 -0
- app/frontend/templates/pages/registration.html +83 -0
- app/frontend/templates/pages/show_pdf.html +98 -0
- app/frontend/templates/pages/show_text.html +47 -0
- app/main.py +41 -0
- app/models.py +105 -0
- app/processor.py +230 -0
- app/prompt.txt +108 -0
- app/prompt_templates/test1.txt +16 -0
- app/prompt_templates/test2.txt +89 -0
- app/prompt_templates/test3.txt +116 -0
- app/rag_generator.py +106 -0
- app/requirements.txt +0 -0
- app/response_parser.py +25 -0
- app/settings.py +97 -0
- docker-compose.yml +21 -0
- requirements.txt +0 -0
- start.sh +12 -3
- templates/base.html +0 -284
- templates/index.html +0 -282
.gitattributes
DELETED
|
@@ -1,35 +0,0 @@
|
|
| 1 |
-
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
-
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
-
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.gitignore
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__
|
| 2 |
+
/app/temp_storage
|
| 3 |
+
/database
|
| 4 |
+
/new_env
|
| 5 |
+
/prompt.txt
|
| 6 |
+
/app/key.py
|
Dockerfile
CHANGED
|
@@ -1,14 +1,42 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
|
|
|
|
|
|
| 5 |
WORKDIR /app
|
| 6 |
-
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
RUN wget https://github.com/qdrant/qdrant/releases/download/v1.11.5/qdrant-x86_64-unknown-linux-gnu.tar.gz \
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# syntax=docker/dockerfile:1
|
| 2 |
+
FROM python:3.11-slim-bookworm
|
| 3 |
+
|
| 4 |
+
# avoid root, but Spaces run as root so this is informational
|
| 5 |
+
RUN addgroup --system app && adduser --system --ingroup app app
|
| 6 |
+
|
| 7 |
WORKDIR /app
|
| 8 |
+
|
| 9 |
+
# system deps for Qdrant and psycopg2, cleanup
|
| 10 |
+
RUN apt-get update \
|
| 11 |
+
&& apt-get install -y --no-install-recommends build-essential wget ca-certificates \
|
| 12 |
+
&& apt-get clean \
|
| 13 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 14 |
+
|
| 15 |
+
# copy and install Python reqs
|
| 16 |
+
COPY app/requirements.txt /app/requirements.txt
|
| 17 |
+
RUN pip install --no-cache-dir -r /app/requirements.txt
|
| 18 |
+
|
| 19 |
+
# download Qdrant binary
|
| 20 |
RUN wget https://github.com/qdrant/qdrant/releases/download/v1.11.5/qdrant-x86_64-unknown-linux-gnu.tar.gz \
|
| 21 |
+
&& tar -xzf qdrant-x86_64-unknown-linux-gnu.tar.gz \
|
| 22 |
+
&& mv qdrant /usr/local/bin/qdrant \
|
| 23 |
+
&& rm qdrant-x86_64-unknown-linux-gnu.tar.gz
|
| 24 |
+
|
| 25 |
+
# copy your application code
|
| 26 |
+
COPY app /app/app
|
| 27 |
+
|
| 28 |
+
# bring in start script
|
| 29 |
+
COPY start.sh /app/start.sh
|
| 30 |
+
RUN chmod +x /app/start.sh
|
| 31 |
+
|
| 32 |
+
# where SQLite DB and temp files will live (persisted by HF)
|
| 33 |
+
RUN mkdir -p /mnt/data/app_database /mnt/data/temp_storage
|
| 34 |
+
|
| 35 |
+
# expose HF-standard port
|
| 36 |
+
EXPOSE 7860
|
| 37 |
+
|
| 38 |
+
# env var for SQLAlchemy
|
| 39 |
+
ENV DATABASE_URL="sqlite:////mnt/data/app_database/app.db"
|
| 40 |
+
|
| 41 |
+
# launch both Qdrant and Uvicorn
|
| 42 |
+
CMD ["./start.sh"]
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2025 Danil Popov
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
README.md
CHANGED
|
@@ -1,12 +1,74 @@
|
|
| 1 |
-
--
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
-
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# The-Ultimate-RAG
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
[S25] The Ultimate RAG is an Innopolis University software project that generates cited responses from a local database.
|
| 6 |
+
|
| 7 |
+
## Prerequisites
|
| 8 |
+
|
| 9 |
+
Before you begin, ensure the following is installed on your machine:
|
| 10 |
+
- [Python](https://www.python.org/)
|
| 11 |
+
- [Docker](https://www.docker.com/get-started/)
|
| 12 |
+
|
| 13 |
+
## Installation
|
| 14 |
+
|
| 15 |
+
1. **Clone the repository**
|
| 16 |
+
```bash
|
| 17 |
+
git clone https://github.com/PopovDanil/The-Ultimate-RAG
|
| 18 |
+
cd The-Ultimate-RAG
|
| 19 |
+
```
|
| 20 |
+
2. **Set up a virtual environment (recommended)**
|
| 21 |
+
|
| 22 |
+
To isolate project dependencies and avoid conflicts, create a virtual environment:
|
| 23 |
+
- **On Unix/Linux/macOS:**
|
| 24 |
+
```bash
|
| 25 |
+
python3 -m venv env
|
| 26 |
+
source env/bin/activate
|
| 27 |
+
```
|
| 28 |
+
- **On Windows:**
|
| 29 |
+
```bash
|
| 30 |
+
python -m venv env
|
| 31 |
+
env\Scripts\activate
|
| 32 |
+
```
|
| 33 |
+
3. **Install required libraries**
|
| 34 |
+
|
| 35 |
+
Within the activated virtual environment, install the dependencies:
|
| 36 |
+
```bash
|
| 37 |
+
pip install -r ./app/requirements.txt
|
| 38 |
+
```
|
| 39 |
+
*Note:* ensure you are in the virtual environment before running the command
|
| 40 |
+
|
| 41 |
+
4. **Set up Docker**
|
| 42 |
+
- Ensure Docker is running on your machine
|
| 43 |
+
- Open a terminal, navigate to project directory, and run:
|
| 44 |
+
```bash
|
| 45 |
+
docker-compose up --build
|
| 46 |
+
```
|
| 47 |
+
*Note:* The initial build may take 10–20 minutes, as it needs to download large language models and other
|
| 48 |
+
dependencies.
|
| 49 |
+
Later launches will be much faster.
|
| 50 |
+
|
| 51 |
+
5. **Server access**
|
| 52 |
+
|
| 53 |
+
Once the containers are running, visit `http://localhost:5050`. You should see the application’s welcome page
|
| 54 |
+
|
| 55 |
+
To stop the application and shut down all containers, press `Ctrl+C` in the terminal where `docker-compose` is running,
|
| 56 |
+
and then run:
|
| 57 |
+
```bash
|
| 58 |
+
docker-compose down
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
## Usage
|
| 62 |
+
|
| 63 |
+
1. **Upload your file:** click the upload button and select a supported file (`.txt`, `.doc`, `.docx`, or `.pdf`)
|
| 64 |
+
2. **Ask a question**: Once the file is processed, type your question into the prompt box and submit.
|
| 65 |
+
3. **Receive your answer**
|
| 66 |
+
|
| 67 |
+
**A note on performance**
|
| 68 |
+
|
| 69 |
+
Response generation is a computationally intensive task.
|
| 70 |
+
The time to receive an answer may vary depending on your machine's hardware and the complexity of the query.
|
| 71 |
+
|
| 72 |
+
## License
|
| 73 |
+
|
| 74 |
+
This project is licensed under the [MIT License](LICENSE).
|
app.py
DELETED
|
@@ -1,114 +0,0 @@
|
|
| 1 |
-
from fastapi import FastAPI, Depends, HTTPException
|
| 2 |
-
from sqlalchemy import Column, Integer, String, create_engine
|
| 3 |
-
from sqlalchemy.ext.declarative import declarative_base
|
| 4 |
-
from sqlalchemy.orm import sessionmaker, Session
|
| 5 |
-
from pydantic import BaseModel
|
| 6 |
-
from typing import List
|
| 7 |
-
from qdrant_client import QdrantClient
|
| 8 |
-
from qdrant_client.models import Distance, VectorParams, PointStruct
|
| 9 |
-
import os
|
| 10 |
-
|
| 11 |
-
# Initialize FastAPI app
|
| 12 |
-
api = FastAPI()
|
| 13 |
-
|
| 14 |
-
# Get the database URL from an environment variable
|
| 15 |
-
DATABASE_URL = os.getenv("DATABASE_URL")
|
| 16 |
-
|
| 17 |
-
# Set up SQLAlchemy engine and session for PostgreSQL
|
| 18 |
-
engine = create_engine(DATABASE_URL)
|
| 19 |
-
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
| 20 |
-
|
| 21 |
-
# Define the SQLAlchemy base
|
| 22 |
-
Base = declarative_base()
|
| 23 |
-
|
| 24 |
-
# Define the User model for the PostgreSQL database
|
| 25 |
-
class User(Base):
|
| 26 |
-
__tablename__ = "users"
|
| 27 |
-
id = Column(Integer, primary_key=True, index=True)
|
| 28 |
-
name = Column(String, index=True)
|
| 29 |
-
email = Column(String, unique=True, index=True)
|
| 30 |
-
|
| 31 |
-
# Set up Qdrant client
|
| 32 |
-
client = QdrantClient("localhost", port=6333)
|
| 33 |
-
|
| 34 |
-
# Startup event to create database tables and initialize Qdrant collection
|
| 35 |
-
@api.on_event("startup")
|
| 36 |
-
def startup_event():
|
| 37 |
-
# Create PostgreSQL database tables
|
| 38 |
-
Base.metadata.create_all(bind=engine)
|
| 39 |
-
# Initialize Qdrant collection if it doesn’t exist
|
| 40 |
-
collections = client.get_collections()
|
| 41 |
-
if "my_collection" not in [col.name for col in collections.collections]:
|
| 42 |
-
client.create_collection(
|
| 43 |
-
collection_name="my_collection",
|
| 44 |
-
vectors_config=VectorParams(size=10, distance=Distance.COSINE),
|
| 45 |
-
)
|
| 46 |
-
|
| 47 |
-
# Pydantic models for PostgreSQL user endpoints
|
| 48 |
-
class UserCreate(BaseModel):
|
| 49 |
-
name: str
|
| 50 |
-
email: str
|
| 51 |
-
|
| 52 |
-
class UserResponse(BaseModel):
|
| 53 |
-
id: int
|
| 54 |
-
name: str
|
| 55 |
-
email: str
|
| 56 |
-
|
| 57 |
-
class Config:
|
| 58 |
-
orm_mode = True
|
| 59 |
-
|
| 60 |
-
# Pydantic models for Qdrant endpoints
|
| 61 |
-
class AddPointRequest(BaseModel):
|
| 62 |
-
id: int
|
| 63 |
-
vector: List[float]
|
| 64 |
-
|
| 65 |
-
class SearchRequest(BaseModel):
|
| 66 |
-
vector: List[float]
|
| 67 |
-
top_k: int = 5
|
| 68 |
-
|
| 69 |
-
# Dependency to get the PostgreSQL database session
|
| 70 |
-
def get_db():
|
| 71 |
-
db = SessionLocal()
|
| 72 |
-
try:
|
| 73 |
-
yield db
|
| 74 |
-
finally:
|
| 75 |
-
db.close()
|
| 76 |
-
|
| 77 |
-
# Root endpoint for testing
|
| 78 |
-
@api.get("/")
|
| 79 |
-
def root():
|
| 80 |
-
return {"message": "Hello World"}
|
| 81 |
-
|
| 82 |
-
# Endpoint to create a user in PostgreSQL
|
| 83 |
-
@api.post("/users/", response_model=UserResponse)
|
| 84 |
-
def create_user(user: UserCreate, db: Session = Depends(get_db)):
|
| 85 |
-
db_user = User(name=user.name, email=user.email)
|
| 86 |
-
db.add(db_user)
|
| 87 |
-
db.commit()
|
| 88 |
-
db.refresh(db_user)
|
| 89 |
-
return db_user
|
| 90 |
-
|
| 91 |
-
# Endpoint to get all users from PostgreSQL
|
| 92 |
-
@api.get("/users/", response_model=list[UserResponse])
|
| 93 |
-
def read_users(db: Session = Depends(get_db)):
|
| 94 |
-
users = db.query(User).all()
|
| 95 |
-
return users
|
| 96 |
-
|
| 97 |
-
# Endpoint to add a point to Qdrant
|
| 98 |
-
@api.post("/add_point")
|
| 99 |
-
def add_point(request: AddPointRequest):
|
| 100 |
-
client.upsert(
|
| 101 |
-
collection_name="my_collection",
|
| 102 |
-
points=[PointStruct(id=request.id, vector=request.vector)],
|
| 103 |
-
)
|
| 104 |
-
return {"status": "ok"}
|
| 105 |
-
|
| 106 |
-
# Endpoint to search in Qdrant
|
| 107 |
-
@api.post("/search")
|
| 108 |
-
def search(request: SearchRequest):
|
| 109 |
-
results = client.search(
|
| 110 |
-
collection_name="my_collection",
|
| 111 |
-
query_vector=request.vector,
|
| 112 |
-
limit=request.top_k,
|
| 113 |
-
)
|
| 114 |
-
return {"results": [{"id": res.id, "score": res.score} for res in results]}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/__init__.py
ADDED
|
File without changes
|
app/api.py
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI, UploadFile, Form, File, HTTPException, Response, Request, Depends
|
| 2 |
+
import uuid
|
| 3 |
+
from app.backend.models.users import User
|
| 4 |
+
from fastapi.staticfiles import StaticFiles
|
| 5 |
+
import os
|
| 6 |
+
from app.rag_generator import RagSystem
|
| 7 |
+
from fastapi.responses import HTMLResponse, FileResponse, RedirectResponse
|
| 8 |
+
from app.settings import base_path, url_user_not_required
|
| 9 |
+
from typing import Optional
|
| 10 |
+
from app.response_parser import add_links
|
| 11 |
+
from app.document_validator import path_is_valid
|
| 12 |
+
from app.backend.controllers.users import create_user, authenticate_user, check_cookie, clear_cookie, get_current_user
|
| 13 |
+
from app.backend.controllers.schemas import SUser
|
| 14 |
+
from app.backend.controllers.chats import create_new_chat
|
| 15 |
+
from fastapi.templating import Jinja2Templates
|
| 16 |
+
|
| 17 |
+
# TODO: implement a better TextHandler
|
| 18 |
+
# TODO: optionally implement DocHandler
|
| 19 |
+
|
| 20 |
+
api = FastAPI()
|
| 21 |
+
rag = None
|
| 22 |
+
api.mount("/pdfs", StaticFiles(directory=os.path.join(base_path, "temp_storage", "pdfs")), name="pdfs")
|
| 23 |
+
api.mount("/static", StaticFiles(directory=os.path.join(base_path, "frontend", "static")), name="static")
|
| 24 |
+
templates = Jinja2Templates(directory=os.path.join(base_path, "frontend", "templates"))
|
| 25 |
+
|
| 26 |
+
def initialize_rag() -> RagSystem:
|
| 27 |
+
global rag
|
| 28 |
+
if rag is None:
|
| 29 |
+
rag = RagSystem()
|
| 30 |
+
return rag
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
'''
|
| 34 |
+
Updates response context and adds context of navbar (role, instance(or none)) and footer (none)
|
| 35 |
+
'''
|
| 36 |
+
def extend_context(context: dict):
|
| 37 |
+
user = get_current_user(context.get("request"))
|
| 38 |
+
navbar = {
|
| 39 |
+
"navbar": True,
|
| 40 |
+
"navbar_path": "components/navbar.html",
|
| 41 |
+
"navbar_context": {
|
| 42 |
+
"user": {
|
| 43 |
+
"role": "user" if user else "guest",
|
| 44 |
+
"instance": user
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
footer = {
|
| 49 |
+
"footer": False,
|
| 50 |
+
"footer_context": None
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
context.update(**navbar)
|
| 54 |
+
context.update(**footer)
|
| 55 |
+
|
| 56 |
+
return context
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def PDFHandler(request: Request, path: str, page: int) -> HTMLResponse:
|
| 60 |
+
filename = os.path.basename(path)
|
| 61 |
+
url_path = f"/pdfs/{filename}"
|
| 62 |
+
current_template = "pages/show_pdf.html"
|
| 63 |
+
return templates.TemplateResponse(
|
| 64 |
+
current_template,
|
| 65 |
+
extend_context({
|
| 66 |
+
"request": request,
|
| 67 |
+
"page": str(page or 1),
|
| 68 |
+
"url_path": url_path,
|
| 69 |
+
"user": get_current_user(request)
|
| 70 |
+
})
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def TextHandler(request: Request, path: str, lines: str) -> HTMLResponse:
|
| 75 |
+
file_content = ""
|
| 76 |
+
with open(path, "r") as f:
|
| 77 |
+
file_content = f.read()
|
| 78 |
+
|
| 79 |
+
start_line, end_line = map(int, lines.split('-'))
|
| 80 |
+
|
| 81 |
+
text_before_citation = []
|
| 82 |
+
text_after_citation = []
|
| 83 |
+
citation = []
|
| 84 |
+
anchor_added = False
|
| 85 |
+
|
| 86 |
+
for index, line in enumerate(file_content.split('\n')):
|
| 87 |
+
if line == "" or line == "\n":
|
| 88 |
+
continue
|
| 89 |
+
if index + 1 < start_line:
|
| 90 |
+
text_before_citation.append(line)
|
| 91 |
+
elif end_line < index + 1:
|
| 92 |
+
text_after_citation.append(line)
|
| 93 |
+
else:
|
| 94 |
+
anchor_added = True
|
| 95 |
+
citation.append(line)
|
| 96 |
+
|
| 97 |
+
current_template = "pages/show_text.html"
|
| 98 |
+
|
| 99 |
+
return templates.TemplateResponse(
|
| 100 |
+
current_template,
|
| 101 |
+
extend_context({
|
| 102 |
+
"request": request,
|
| 103 |
+
"text_before_citation": text_before_citation,
|
| 104 |
+
"text_after_citation": text_after_citation,
|
| 105 |
+
"citation": citation,
|
| 106 |
+
"anchor_added": anchor_added,
|
| 107 |
+
"user": get_current_user(request)
|
| 108 |
+
})
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
'''
|
| 113 |
+
Optional handler
|
| 114 |
+
'''
|
| 115 |
+
def DocHandler():
|
| 116 |
+
pass
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
# <--------------------------------- Middleware --------------------------------->
|
| 120 |
+
# NOTE: carefully read documentation to require_user
|
| 121 |
+
|
| 122 |
+
'''
|
| 123 |
+
Special class to have an opportunity to redirect user to login page in middleware
|
| 124 |
+
'''
|
| 125 |
+
class AwaitableResponse:
|
| 126 |
+
def __init__(self, response: Response):
|
| 127 |
+
self.response = response
|
| 128 |
+
|
| 129 |
+
def __await__(self):
|
| 130 |
+
yield
|
| 131 |
+
return self.response
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
'''
|
| 135 |
+
TODO: remove KOSTYLY -> find better way to skip requesting to login while showing pdf
|
| 136 |
+
|
| 137 |
+
Middleware that requires user to log in into the system before accessing any utl
|
| 138 |
+
|
| 139 |
+
NOTE: For now it is applied to all routes, but if you want to skip any, add it to the
|
| 140 |
+
url_user_not_required list in settings.py (/ should be removed)
|
| 141 |
+
'''
|
| 142 |
+
@api.middleware("http")
|
| 143 |
+
async def require_user(request: Request, call_next):
|
| 144 |
+
print(request.url.path, request.method)
|
| 145 |
+
|
| 146 |
+
awaitable_response = AwaitableResponse(RedirectResponse("/login", status_code=303))
|
| 147 |
+
stripped_path = request.url.path.strip('/')
|
| 148 |
+
|
| 149 |
+
if stripped_path in url_user_not_required \
|
| 150 |
+
or stripped_path.startswith("pdfs") \
|
| 151 |
+
or "static/styles.css" in stripped_path \
|
| 152 |
+
or "favicon.ico" in stripped_path:
|
| 153 |
+
return await call_next(request)
|
| 154 |
+
|
| 155 |
+
user = get_current_user(request)
|
| 156 |
+
if user is None:
|
| 157 |
+
return await awaitable_response
|
| 158 |
+
|
| 159 |
+
response = await call_next(request)
|
| 160 |
+
return response
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
# <--------------------------------- Common routes --------------------------------->
|
| 164 |
+
# @api.get("/")
|
| 165 |
+
# def root(request: Request):
|
| 166 |
+
# current_template = "pages/main.html"
|
| 167 |
+
# return templates.TemplateResponse(current_template, extend_context({"request": request}))
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
@api.get("/")
|
| 171 |
+
def root(request: Request):
|
| 172 |
+
current_template = "pages/chat.html"
|
| 173 |
+
return templates.TemplateResponse(current_template,
|
| 174 |
+
extend_context({
|
| 175 |
+
"request": request,
|
| 176 |
+
"user": get_current_user(request)
|
| 177 |
+
})
|
| 178 |
+
)
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
@api.post("/message_with_docs")
|
| 182 |
+
async def create_prompt(files: list[UploadFile] = File(...), prompt: str = Form(...)):
|
| 183 |
+
docs = []
|
| 184 |
+
rag = initialize_rag()
|
| 185 |
+
|
| 186 |
+
try:
|
| 187 |
+
|
| 188 |
+
for file in files:
|
| 189 |
+
content = await file.read()
|
| 190 |
+
temp_storage = os.path.join(base_path, "temp_storage")
|
| 191 |
+
os.makedirs(temp_storage, exist_ok=True)
|
| 192 |
+
|
| 193 |
+
if file.filename.endswith('.pdf'):
|
| 194 |
+
saved_file = os.path.join(temp_storage, "pdfs", str(uuid.uuid4()) + ".pdf")
|
| 195 |
+
else:
|
| 196 |
+
saved_file = os.path.join(temp_storage, str(uuid.uuid4()) + "." + file.filename.split('.')[-1])
|
| 197 |
+
|
| 198 |
+
with open(saved_file, "wb") as f:
|
| 199 |
+
f.write(content)
|
| 200 |
+
|
| 201 |
+
docs.append(saved_file)
|
| 202 |
+
|
| 203 |
+
if len(files) > 0:
|
| 204 |
+
rag.upload_documents(docs)
|
| 205 |
+
|
| 206 |
+
response_raw = rag.generate_response(user_prompt=prompt)
|
| 207 |
+
response = add_links(response_raw)
|
| 208 |
+
|
| 209 |
+
return {"response": response, "status": 200}
|
| 210 |
+
|
| 211 |
+
except Exception as e:
|
| 212 |
+
print("!!!ERROR!!!")
|
| 213 |
+
print(e)
|
| 214 |
+
|
| 215 |
+
# finally:
|
| 216 |
+
# for file in files:
|
| 217 |
+
# temp_storage = os.path.join(base_path, "temp_storage")
|
| 218 |
+
# saved_file = os.path.join(temp_storage, file.filename)
|
| 219 |
+
# os.remove(saved_file)
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
@api.get("/viewer")
|
| 223 |
+
def show_document(request: Request, path: str, page: Optional[int] = 1, lines: Optional[str] = "1-1", start: Optional[int] = 0):
|
| 224 |
+
if not path_is_valid(path):
|
| 225 |
+
return HTTPException(status_code=404, detail="Document not found")
|
| 226 |
+
|
| 227 |
+
ext = path.split(".")[-1]
|
| 228 |
+
if ext == 'pdf':
|
| 229 |
+
return PDFHandler(request, path=path, page=page)
|
| 230 |
+
elif ext in ('txt', 'csv', 'md'):
|
| 231 |
+
return TextHandler(request, path=path, lines=lines)
|
| 232 |
+
elif ext in ('docx', 'doc'):
|
| 233 |
+
return TextHandler(request, path=path, lines=lines) # should be a bit different handler
|
| 234 |
+
else:
|
| 235 |
+
return FileResponse(path=path)
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
# <--------------------------------- Get --------------------------------->
|
| 239 |
+
@api.get("/new_user")
|
| 240 |
+
def new_user(request: Request):
|
| 241 |
+
current_template = "pages/registration.html"
|
| 242 |
+
return templates.TemplateResponse(current_template, extend_context({"request": request}))
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
@api.get("/login")
|
| 246 |
+
def login(request: Request):
|
| 247 |
+
current_template = "pages/login.html"
|
| 248 |
+
return templates.TemplateResponse(current_template, extend_context({"request": request}))
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
@api.get("/cookie_test")
|
| 252 |
+
def test_cookie(request: Request):
|
| 253 |
+
return check_cookie(request)
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
'''
|
| 257 |
+
Use only for testing. For now, provides user info for logged ones, and redirects to
|
| 258 |
+
login in other case
|
| 259 |
+
'''
|
| 260 |
+
@api.get("/test")
|
| 261 |
+
def test(request: Request, user: User = Depends(get_current_user)):
|
| 262 |
+
return {
|
| 263 |
+
"user": {
|
| 264 |
+
"email": user.email,
|
| 265 |
+
"password_hash": user.password_hash,
|
| 266 |
+
# "chats": user.chats, # Note: it will rise error since due to the optimization associated fields are not loaded
|
| 267 |
+
# it is just a reference, but the session is closed, however you are trying to get access to the data through this session
|
| 268 |
+
}
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
@api.get("/chats/id={chat_id}")
|
| 273 |
+
def show_chat(chat_id: int):
|
| 274 |
+
return {"chat_id": chat_id}
|
| 275 |
+
|
| 276 |
+
|
| 277 |
+
@api.get("/logout")
|
| 278 |
+
def logout(response: Response):
|
| 279 |
+
return clear_cookie(response)
|
| 280 |
+
# <--------------------------------- Post --------------------------------->
|
| 281 |
+
@api.post("/new_user")
|
| 282 |
+
def new_user(response: Response, user: SUser):
|
| 283 |
+
return create_user(response, user.email, user.password)
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
@api.post("/login")
|
| 288 |
+
def login(response: Response, user: SUser):
|
| 289 |
+
return authenticate_user(response, user.email, user.password)
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
@api.post("/new_chat")
|
| 293 |
+
def create_chat(request: Request, title: Optional[str] = "new chat", user: User = Depends(get_current_user)):
|
| 294 |
+
url = create_new_chat(title, user)
|
| 295 |
+
return RedirectResponse(url, status_code=303)
|
app/backend/__init__.py
ADDED
|
File without changes
|
app/backend/controllers/__init__.py
ADDED
|
File without changes
|
app/backend/controllers/base_controller.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy import create_engine
|
| 2 |
+
from app.settings import postgres_client_config
|
| 3 |
+
|
| 4 |
+
engine = create_engine(**postgres_client_config)
|
app/backend/controllers/chats.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.backend.models.users import User
|
| 2 |
+
from app.backend.models.chats import new_chat
|
| 3 |
+
|
| 4 |
+
def create_new_chat(title: str | None, user: User) -> str:
|
| 5 |
+
return f"/chats/id={new_chat(title, user)}"
|
app/backend/controllers/schemas.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel, Field, EmailStr, field_validator
|
| 2 |
+
import re
|
| 3 |
+
|
| 4 |
+
class SUser(BaseModel):
|
| 5 |
+
email: EmailStr
|
| 6 |
+
password: str = Field(default=..., min_length=8, max_length=32)
|
| 7 |
+
|
| 8 |
+
@field_validator('password', mode='before')
|
| 9 |
+
def validate_password(cls, password_to_validate):
|
| 10 |
+
"""
|
| 11 |
+
Validates the strength of the password.
|
| 12 |
+
|
| 13 |
+
The password **must** contain:
|
| 14 |
+
- At least one digit
|
| 15 |
+
- At least one special character
|
| 16 |
+
- At least one uppercase character
|
| 17 |
+
- At least one lowercase character
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
if not re.search(r"\d", password_to_validate):
|
| 21 |
+
raise ValueError("Password must contain at least one number.")
|
| 22 |
+
|
| 23 |
+
if not re.search(r"[!@#$%^&*()_+\-=\[\]{};:\'\",.<>?`~]", password_to_validate):
|
| 24 |
+
raise ValueError("Password must contain at least one special symbol.")
|
| 25 |
+
|
| 26 |
+
if not re.search(r"[A-Z]", password_to_validate):
|
| 27 |
+
raise ValueError("Password must contain at least one uppercase letter.")
|
| 28 |
+
|
| 29 |
+
if not re.search(r"[a-z]", password_to_validate):
|
| 30 |
+
raise ValueError("Password must contain at least one lowercase letter.")
|
| 31 |
+
|
| 32 |
+
return password_to_validate
|
app/backend/controllers/users.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.backend.models.users import User, add_new_user, find_user_by_email, find_user_by_access_string, update_user
|
| 2 |
+
from bcrypt import gensalt, hashpw, checkpw
|
| 3 |
+
from app.settings import very_secret_pepper, jwt_algorithm, max_cookie_lifetime
|
| 4 |
+
from fastapi import HTTPException
|
| 5 |
+
import jwt
|
| 6 |
+
from datetime import datetime, timedelta
|
| 7 |
+
from fastapi import Response, Request
|
| 8 |
+
from secrets import token_urlsafe
|
| 9 |
+
import hmac
|
| 10 |
+
import hashlib
|
| 11 |
+
|
| 12 |
+
# A vot nado bilo izuchat kak web dev rabotaet
|
| 13 |
+
|
| 14 |
+
'''
|
| 15 |
+
Creates a jwt token by access string
|
| 16 |
+
|
| 17 |
+
Param:
|
| 18 |
+
access_string - randomly (safe methods) generated string (by default - 16 len)
|
| 19 |
+
expires_delta - time in seconds, defines a token lifetime
|
| 20 |
+
|
| 21 |
+
Returns:
|
| 22 |
+
string with 4 sections (valid jwt token)
|
| 23 |
+
'''
|
| 24 |
+
def create_access_token(access_string: str, expires_delta: timedelta = timedelta(seconds=max_cookie_lifetime)) -> str:
|
| 25 |
+
token_payload = {
|
| 26 |
+
"access_string": access_string,
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
token_payload.update({"exp": datetime.now() + expires_delta})
|
| 30 |
+
encoded_jwt: str = jwt.encode(token_payload, very_secret_pepper, algorithm=jwt_algorithm)
|
| 31 |
+
|
| 32 |
+
return encoded_jwt
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
'''
|
| 36 |
+
Safely creates random string of 16 chars
|
| 37 |
+
'''
|
| 38 |
+
def create_access_string() -> str:
|
| 39 |
+
return token_urlsafe(16)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
'''
|
| 43 |
+
Hashes access string using hmac and sha256
|
| 44 |
+
|
| 45 |
+
We can not use the same methods as we do to save password
|
| 46 |
+
since we need to know a salt to get similar hash, but since
|
| 47 |
+
we put a raw string (non-hashed) we won't be able to guess
|
| 48 |
+
salt
|
| 49 |
+
'''
|
| 50 |
+
def hash_access_string(string: str) -> str:
|
| 51 |
+
return hmac.new(
|
| 52 |
+
key=very_secret_pepper.encode("utf-8"),
|
| 53 |
+
msg=string.encode("utf-8"),
|
| 54 |
+
digestmod=hashlib.sha256
|
| 55 |
+
).hexdigest()
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
'''
|
| 59 |
+
Creates a new user and sets a cookie with jwt token
|
| 60 |
+
|
| 61 |
+
Params:
|
| 62 |
+
response - needed to set a cookie
|
| 63 |
+
...
|
| 64 |
+
|
| 65 |
+
Returns:
|
| 66 |
+
Dict to send a response in JSON
|
| 67 |
+
'''
|
| 68 |
+
def create_user(response: Response, email: str, password: str) -> dict:
|
| 69 |
+
user: User = find_user_by_email(email=email)
|
| 70 |
+
if user is not None:
|
| 71 |
+
return HTTPException(418, "The user with similar email already exists")
|
| 72 |
+
|
| 73 |
+
salt: bytes = gensalt(rounds=16)
|
| 74 |
+
password_hashed: str = hashpw(password.encode("utf-8"), salt).decode("utf-8")
|
| 75 |
+
|
| 76 |
+
access_string: str = create_access_string()
|
| 77 |
+
access_string_hashed: str = hash_access_string(string=access_string)
|
| 78 |
+
|
| 79 |
+
add_new_user(email=email, password_hash=password_hashed, access_string_hash=access_string_hashed)
|
| 80 |
+
|
| 81 |
+
access_token: str = create_access_token(access_string=access_string)
|
| 82 |
+
response.set_cookie(key="access_token", value=access_token, path='/', max_age=max_cookie_lifetime, httponly=True)
|
| 83 |
+
|
| 84 |
+
return {"status": "ok"}
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
'''
|
| 89 |
+
Finds user by email. If user is found, sets a cookie with token
|
| 90 |
+
'''
|
| 91 |
+
def authenticate_user(response: Response, email: str, password: str) -> dict:
|
| 92 |
+
user: User = find_user_by_email(email=email)
|
| 93 |
+
|
| 94 |
+
if not user:
|
| 95 |
+
raise HTTPException(418, "User does not exists")
|
| 96 |
+
|
| 97 |
+
if not checkpw(password.encode('utf-8'), user.password_hash.encode('utf-8')):
|
| 98 |
+
raise HTTPException(418, "Wrong credentials")
|
| 99 |
+
|
| 100 |
+
access_string: str = create_access_string()
|
| 101 |
+
access_string_hashed: str = hash_access_string(string=access_string)
|
| 102 |
+
|
| 103 |
+
update_user(user, access_string_hash=access_string_hashed)
|
| 104 |
+
|
| 105 |
+
access_token = create_access_token(access_string)
|
| 106 |
+
response.set_cookie(key="access_token", value=access_token, path='/', max_age=max_cookie_lifetime, httponly=True)
|
| 107 |
+
|
| 108 |
+
return {"status": "ok"}
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
'''
|
| 112 |
+
Get user from token stored in cookies
|
| 113 |
+
'''
|
| 114 |
+
def get_current_user(request: Request) -> User | None:
|
| 115 |
+
token: str | None = request.cookies.get("access_token")
|
| 116 |
+
if not token:
|
| 117 |
+
return None
|
| 118 |
+
|
| 119 |
+
access_string = jwt.decode(
|
| 120 |
+
jwt=bytes(token, encoding='utf-8'),
|
| 121 |
+
key=very_secret_pepper,
|
| 122 |
+
algorithms=[jwt_algorithm]
|
| 123 |
+
).get('access_string')
|
| 124 |
+
|
| 125 |
+
user = find_user_by_access_string(hash_access_string(access_string))
|
| 126 |
+
if not user:
|
| 127 |
+
return None
|
| 128 |
+
|
| 129 |
+
return user
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
'''
|
| 133 |
+
Checks if cookie with access token is present
|
| 134 |
+
'''
|
| 135 |
+
def check_cookie(request: Request) -> dict:
|
| 136 |
+
result = {"token": "No token is present"}
|
| 137 |
+
token = request.cookies.get("access_token")
|
| 138 |
+
if token:
|
| 139 |
+
result["token"] = token
|
| 140 |
+
return result
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def clear_cookie(response: Response) -> dict:
|
| 144 |
+
response.set_cookie(key="access_token", value="", httponly=True)
|
| 145 |
+
return {"status": "ok"}
|
app/backend/models/__init__.py
ADDED
|
File without changes
|
app/backend/models/base_model.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy.orm import DeclarativeBase
|
| 2 |
+
from sqlalchemy import DateTime, Column
|
| 3 |
+
from sqlalchemy.sql import func
|
| 4 |
+
|
| 5 |
+
class Base(DeclarativeBase):
|
| 6 |
+
__abstract__ = True
|
| 7 |
+
created_at = Column("created_at", DateTime, default=func.now())
|
| 8 |
+
deleted_at = Column("deleted_at", DateTime, nullable=True)
|
| 9 |
+
updated_at = Column("updated_at", DateTime, nullable=True)
|
app/backend/models/chats.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.backend.models.base_model import Base
|
| 2 |
+
from app.backend.models.users import User
|
| 3 |
+
from sqlalchemy import Integer, String, Column, ForeignKey
|
| 4 |
+
from sqlalchemy.orm import relationship, Session
|
| 5 |
+
from app.backend.controllers.base_controller import engine
|
| 6 |
+
|
| 7 |
+
class Chat(Base):
|
| 8 |
+
__tablename__ = "chats"
|
| 9 |
+
id = Column("id", Integer, autoincrement=True, primary_key=True, unique=True)
|
| 10 |
+
title = Column("title", String, nullable=True)
|
| 11 |
+
user_id = Column(Integer, ForeignKey("users.id"))
|
| 12 |
+
user = relationship("User", back_populates="chats")
|
| 13 |
+
messages = relationship("Message", back_populates="chat")
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def new_chat(title: str | None, user: User) -> int:
|
| 17 |
+
id = None
|
| 18 |
+
with Session(autoflush=False, bind=engine) as db:
|
| 19 |
+
new_chat = Chat(user_id=user.id, user=user)
|
| 20 |
+
if title:
|
| 21 |
+
new_chat.title = title
|
| 22 |
+
db.add(new_chat)
|
| 23 |
+
db.commit()
|
| 24 |
+
id = new_chat.id
|
| 25 |
+
return id
|
app/backend/models/db_service.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.backend.models.users import User
|
| 2 |
+
from app.backend.models.chats import Chat
|
| 3 |
+
from app.backend.models.messages import Message
|
| 4 |
+
from app.backend.controllers.base_controller import engine
|
| 5 |
+
from app.backend.models.base_model import Base
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def table_exists(name: str) -> bool:
|
| 9 |
+
return engine.dialect.has_table(engine, name)
|
| 10 |
+
|
| 11 |
+
def create_tables() -> None:
|
| 12 |
+
Base.metadata.create_all(engine)
|
| 13 |
+
|
| 14 |
+
def drop_tables() -> None:
|
| 15 |
+
# for now the order matters, so
|
| 16 |
+
# TODO: add cascade deletion for models
|
| 17 |
+
Message.__table__.drop(engine)
|
| 18 |
+
Chat.__table__.drop(engine)
|
| 19 |
+
User.__table__.drop(engine)
|
| 20 |
+
|
| 21 |
+
def automigrate() -> None:
|
| 22 |
+
try:
|
| 23 |
+
drop_tables()
|
| 24 |
+
except Exception as e:
|
| 25 |
+
print(e)
|
| 26 |
+
|
| 27 |
+
create_tables()
|
app/backend/models/messages.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.backend.models.base_model import Base
|
| 2 |
+
from sqlalchemy import Integer, String, Column, ForeignKey, Text
|
| 3 |
+
from sqlalchemy.orm import relationship
|
| 4 |
+
from app.backend.controllers.base_controller import engine
|
| 5 |
+
|
| 6 |
+
class Message(Base):
|
| 7 |
+
__tablename__ = "messages"
|
| 8 |
+
id = Column("id", Integer, autoincrement=True, primary_key=True, unique=True)
|
| 9 |
+
content = Column("text", Text)
|
| 10 |
+
sender = Column("role", String)
|
| 11 |
+
chat_id = Column(Integer, ForeignKey("chats.id"))
|
| 12 |
+
chat = relationship("Chat", back_populates="messages")
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def new_message(chat_id: int, sender: str, content: str):
|
| 16 |
+
pass
|
app/backend/models/users.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy import Column, String, Integer
|
| 2 |
+
from sqlalchemy.orm import relationship, Session
|
| 3 |
+
from app.backend.models.base_model import Base
|
| 4 |
+
from app.backend.controllers.base_controller import engine
|
| 5 |
+
|
| 6 |
+
class User(Base):
|
| 7 |
+
__tablename__ = "users"
|
| 8 |
+
id = Column("id", Integer, autoincrement=True, primary_key=True, unique=True)
|
| 9 |
+
email = Column("email", String, unique=True, nullable=False)
|
| 10 |
+
password_hash = Column("password_hash", String, nullable=False)
|
| 11 |
+
language = Column("language", String, default="English", nullable=False)
|
| 12 |
+
theme = Column("theme", String, default="light", nullable=False)
|
| 13 |
+
access_string_hash = Column("access_string_hash", String, nullable=True)
|
| 14 |
+
chats = relationship("Chat", back_populates="user")
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def add_new_user(email: str, password_hash: str, access_string_hash: str) -> None:
|
| 18 |
+
with Session(autoflush=False, bind=engine) as db:
|
| 19 |
+
db.add(User(email=email, password_hash=password_hash, access_string_hash=access_string_hash))
|
| 20 |
+
db.commit()
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def find_user_by_id(id: int) -> User | None:
|
| 24 |
+
with Session(autoflush=False, bind=engine) as db:
|
| 25 |
+
return db.query(User).where(User.id == id).first()
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def find_user_by_email(email: str) -> User | None:
|
| 29 |
+
with Session(autoflush=False, bind=engine) as db:
|
| 30 |
+
return db.query(User).where(User.email == email).first()
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def find_user_by_access_string(access_string_hash: str) -> User | None:
|
| 34 |
+
with Session(autoflush=False, bind=engine) as db:
|
| 35 |
+
return db.query(User).where(User.access_string_hash == access_string_hash).first()
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def update_user(user: User, language: str = None, theme: str = None, access_string_hash: str = None) -> None:
|
| 39 |
+
with Session(autoflush=False, bind=engine) as db:
|
| 40 |
+
user = db.merge(user)
|
| 41 |
+
if language:
|
| 42 |
+
user.language = language
|
| 43 |
+
if theme:
|
| 44 |
+
user.theme = theme
|
| 45 |
+
if access_string_hash:
|
| 46 |
+
user.access_string_hash = access_string_hash
|
| 47 |
+
db.commit()
|
app/chunks.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import uuid
|
| 2 |
+
|
| 3 |
+
class Chunk:
|
| 4 |
+
'''
|
| 5 |
+
id -> unique number in uuid format, can be tried https://www.uuidgenerator.net/
|
| 6 |
+
start_index -> the index of the first char from the beginning of the original document
|
| 7 |
+
|
| 8 |
+
TODO: implement access modifiers and set of getters and setters
|
| 9 |
+
'''
|
| 10 |
+
def __init__(self, id: uuid.UUID, filename: str, page_number: int, start_index: int, start_line: int, end_line: int, text: str):
|
| 11 |
+
self.id: uuid.UUID = id
|
| 12 |
+
self.filename: str = filename
|
| 13 |
+
self.page_number: int = page_number
|
| 14 |
+
self.start_index: int = start_index
|
| 15 |
+
self.start_line: int = start_line
|
| 16 |
+
self.end_line: int = end_line
|
| 17 |
+
self.text: str = text
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def get_raw_text(self) -> str:
|
| 21 |
+
return self.text
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def get_splitted_text(self) -> list[str]:
|
| 25 |
+
return self.text.split(" ")
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def get_metadata(self) -> dict:
|
| 29 |
+
return {
|
| 30 |
+
"id": self.id,
|
| 31 |
+
"filename": self.filename,
|
| 32 |
+
"page_number": self.page_number,
|
| 33 |
+
"start_index": self.start_index,
|
| 34 |
+
"start_line": self.start_line,
|
| 35 |
+
"end_line": self.end_line,
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
# TODO: remove kostyly
|
| 40 |
+
def __str__(self):
|
| 41 |
+
return (f"Chunk from {self.filename.split('/')[-1]}, "
|
| 42 |
+
f"page - {self.page_number}, "
|
| 43 |
+
f"start - {self.start_line}, "
|
| 44 |
+
f"end - {self.end_line}, "
|
| 45 |
+
f"and text - {self.text[:100]}... ({len(self.text)})\n"
|
| 46 |
+
)
|
| 47 |
+
|
app/database.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from qdrant_client import QdrantClient # main component to provide the access to db
|
| 2 |
+
from qdrant_client.http.models import ScoredPoint
|
| 3 |
+
from qdrant_client.models import VectorParams, Distance, \
|
| 4 |
+
PointStruct # VectorParams -> config of vectors that will be used as primary keys
|
| 5 |
+
from app.models import Embedder # Distance -> defines the metric
|
| 6 |
+
from app.chunks import Chunk # PointStruct -> instance that will be stored in db
|
| 7 |
+
import numpy as np
|
| 8 |
+
from uuid import UUID
|
| 9 |
+
from app.settings import qdrant_client_config, max_delta
|
| 10 |
+
import time
|
| 11 |
+
|
| 12 |
+
# TODO: for now all documents are saved to one db, but what if user wants to get references from his own documents, so temp storage is needed
|
| 13 |
+
|
| 14 |
+
class VectorDatabase:
|
| 15 |
+
def __init__(self, embedder: Embedder, host: str = "qdrant", port: int = 6333):
|
| 16 |
+
self.host: str = host
|
| 17 |
+
self.client: QdrantClient = self._initialize_qdrant_client()
|
| 18 |
+
self.collection_name: str = "document_chunks"
|
| 19 |
+
self.embedder: Embedder = embedder # embedder is used to convert a user's query
|
| 20 |
+
self.already_stored: np.array[np.array] = np.array([]).reshape(0, embedder.get_vector_dimensionality()) # should be already normalized
|
| 21 |
+
|
| 22 |
+
if not self._check_collection_exists():
|
| 23 |
+
self._create_collection()
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def store(self, chunks: list[Chunk], batch_size: int = 1000) -> None:
|
| 27 |
+
points: list[PointStruct] = []
|
| 28 |
+
|
| 29 |
+
vectors = self.embedder.encode([chunk.get_raw_text() for chunk in chunks])
|
| 30 |
+
|
| 31 |
+
for vector, chunk in zip(vectors, chunks):
|
| 32 |
+
if self.accept_vector(vector):
|
| 33 |
+
points.append(PointStruct(
|
| 34 |
+
id=str(chunk.id),
|
| 35 |
+
vector=vector,
|
| 36 |
+
payload={"metadata": chunk.get_metadata(), "text": chunk.get_raw_text()}
|
| 37 |
+
))
|
| 38 |
+
|
| 39 |
+
if len(points):
|
| 40 |
+
for group in range(0, len(points), batch_size):
|
| 41 |
+
self.client.upsert(
|
| 42 |
+
collection_name=self.collection_name,
|
| 43 |
+
points=points[group : group + batch_size],
|
| 44 |
+
wait=False,
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
'''
|
| 49 |
+
Measures a cosine of angle between tow vectors
|
| 50 |
+
'''
|
| 51 |
+
def cosine_similarity(self, vec1, vec2):
|
| 52 |
+
return vec1 @ vec2 / (np.linalg.norm(vec1) * np.linalg.norm(vec2))
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
'''
|
| 56 |
+
Defines weather the vector should be stored in the db by searching for the most
|
| 57 |
+
similar one
|
| 58 |
+
'''
|
| 59 |
+
def accept_vector(self, vector: np.array) -> bool:
|
| 60 |
+
most_similar = self.client.query_points(
|
| 61 |
+
collection_name=self.collection_name,
|
| 62 |
+
query=vector,
|
| 63 |
+
limit=1,
|
| 64 |
+
with_vectors=True
|
| 65 |
+
).points
|
| 66 |
+
|
| 67 |
+
if not len(most_similar):
|
| 68 |
+
return True
|
| 69 |
+
else:
|
| 70 |
+
most_similar = most_similar[0]
|
| 71 |
+
|
| 72 |
+
if 1 - self.cosine_similarity(vector, most_similar.vector) < max_delta:
|
| 73 |
+
return False
|
| 74 |
+
return True
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
'''
|
| 78 |
+
According to tests, re-ranker needs ~7-10 chunks to generate the most accurate hit
|
| 79 |
+
|
| 80 |
+
TODO: implement hybrid search
|
| 81 |
+
'''
|
| 82 |
+
|
| 83 |
+
def search(self, query: str, top_k: int = 5) -> list[Chunk]:
|
| 84 |
+
query_embedded: np.ndarray = self.embedder.encode(query)
|
| 85 |
+
|
| 86 |
+
points: list[ScoredPoint] = self.client.query_points(
|
| 87 |
+
collection_name=self.collection_name,
|
| 88 |
+
query=query_embedded,
|
| 89 |
+
limit=top_k
|
| 90 |
+
).points
|
| 91 |
+
|
| 92 |
+
return [
|
| 93 |
+
Chunk(
|
| 94 |
+
id=UUID(point.payload.get("metadata", {}).get("id", "")),
|
| 95 |
+
filename=point.payload.get("metadata", {}).get("filename", ""),
|
| 96 |
+
page_number=point.payload.get("metadata", {}).get("page_number", 0),
|
| 97 |
+
start_index=point.payload.get("metadata", {}).get("start_index", 0),
|
| 98 |
+
start_line=point.payload.get("metadata", {}).get("start_line", 0),
|
| 99 |
+
end_line=point.payload.get("metadata", {}).get("end_line", 0),
|
| 100 |
+
text=point.payload.get("text", "")
|
| 101 |
+
) for point in points
|
| 102 |
+
]
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def _initialize_qdrant_client(self, max_retries=5, delay=2) -> QdrantClient:
|
| 106 |
+
for attempt in range(max_retries):
|
| 107 |
+
try:
|
| 108 |
+
client = QdrantClient(**qdrant_client_config)
|
| 109 |
+
client.get_collections()
|
| 110 |
+
return client
|
| 111 |
+
except Exception as e:
|
| 112 |
+
if attempt == max_retries - 1:
|
| 113 |
+
raise ConnectionError(
|
| 114 |
+
f"Failed to connect to Qdrant server after {max_retries} attempts. "
|
| 115 |
+
f"Last error: {str(e)}"
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
print(f"Connection attempt {attempt + 1} out of {max_retries} failed. "
|
| 119 |
+
f"Retrying in {delay} seconds...")
|
| 120 |
+
|
| 121 |
+
time.sleep(delay)
|
| 122 |
+
delay *= 2
|
| 123 |
+
|
| 124 |
+
def _check_collection_exists(self) -> bool:
|
| 125 |
+
try:
|
| 126 |
+
return self.client.collection_exists(self.collection_name)
|
| 127 |
+
except Exception as e:
|
| 128 |
+
raise ConnectionError(
|
| 129 |
+
f"Failed to check collection {self.collection_name} exists. Last error: {str(e)}"
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
def _create_collection(self) -> None:
|
| 133 |
+
try:
|
| 134 |
+
self.client.create_collection(
|
| 135 |
+
collection_name=self.collection_name,
|
| 136 |
+
vectors_config=VectorParams(
|
| 137 |
+
size=self.embedder.get_vector_dimensionality(),
|
| 138 |
+
distance=Distance.COSINE
|
| 139 |
+
)
|
| 140 |
+
)
|
| 141 |
+
except Exception as e:
|
| 142 |
+
raise RuntimeError(f"Failed to create collection {self.collection_name}: {str(e)}")
|
| 143 |
+
|
| 144 |
+
def __del__(self):
|
| 145 |
+
if hasattr(self, "client"):
|
| 146 |
+
self.client.close()
|
app/document_validator.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
|
| 3 |
+
'''
|
| 4 |
+
Checks if the given path is valid and file exists
|
| 5 |
+
'''
|
| 6 |
+
def path_is_valid(path: str) -> bool:
|
| 7 |
+
return os.path.exists(path)
|
app/frontend/static/styles.css
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.chat-container {
|
| 2 |
+
display: flex;
|
| 3 |
+
width: 100%;
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
.chat-body {
|
| 7 |
+
flex: 1;
|
| 8 |
+
background-color: #f8f9fa;
|
| 9 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
.hero-section {
|
| 13 |
+
background: linear-gradient(135deg, #6e8efb, #a777e3);
|
| 14 |
+
color: white;
|
| 15 |
+
border-radius: 0 0 20px 20px;
|
| 16 |
+
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
.search-container {
|
| 20 |
+
max-width: 800px;
|
| 21 |
+
margin: 0 auto;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
.search-box {
|
| 25 |
+
border-radius: 10px;
|
| 26 |
+
border: none;
|
| 27 |
+
padding: 15px 25px;
|
| 28 |
+
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
.btn-search {
|
| 32 |
+
border-radius: 10px;
|
| 33 |
+
padding: 15px 30px;
|
| 34 |
+
background-color: #4e44ce;
|
| 35 |
+
border: none;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
.file-upload {
|
| 39 |
+
background: white;
|
| 40 |
+
border-radius: 10px;
|
| 41 |
+
padding: 20px;
|
| 42 |
+
margin-bottom: 20px;
|
| 43 |
+
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
.file-list {
|
| 47 |
+
margin-top: 10px;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
.file-item {
|
| 51 |
+
background: #f1f3ff;
|
| 52 |
+
border-radius: 5px;
|
| 53 |
+
padding: 8px 12px;
|
| 54 |
+
margin-bottom: 5px;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
.login-body {
|
| 58 |
+
font-family: Arial, sans-serif;
|
| 59 |
+
max-width: 400px;
|
| 60 |
+
margin: 0 auto;
|
| 61 |
+
padding: 20px;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
.form-group {
|
| 65 |
+
margin-bottom: 15px;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
.login-label {
|
| 69 |
+
display: block;
|
| 70 |
+
margin-bottom: 5px;
|
| 71 |
+
font-weight: bold;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
.input-field {
|
| 75 |
+
width: 100%;
|
| 76 |
+
padding: 8px;
|
| 77 |
+
border: 1px solid #ddd;
|
| 78 |
+
border-radius: 4px;
|
| 79 |
+
box-sizing: border-box;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
.login-button {
|
| 83 |
+
background-color: #4CAF50;
|
| 84 |
+
color: white;
|
| 85 |
+
padding: 10px 15px;
|
| 86 |
+
border: none;
|
| 87 |
+
border-radius: 4px;
|
| 88 |
+
cursor: pointer;
|
| 89 |
+
font-size: 16px;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
.login-button :hover {
|
| 93 |
+
background-color: #45a049;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
.error {
|
| 97 |
+
color: red;
|
| 98 |
+
font-size: 14px;
|
| 99 |
+
margin-top: 5px;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
#pdf-container {
|
| 103 |
+
margin: 0 auto;
|
| 104 |
+
max-width: 100%;
|
| 105 |
+
overflow-x: auto;
|
| 106 |
+
text-align: center;
|
| 107 |
+
padding: 20px 0;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
#pdf-canvas {
|
| 111 |
+
margin: 0 auto;
|
| 112 |
+
display: block;
|
| 113 |
+
max-width: 100%;
|
| 114 |
+
box-shadow: 0 0 5px rgba(0,0,0,0.2);
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
/* Fix the page input container layout */
|
| 118 |
+
.page-input-container {
|
| 119 |
+
position: relative;
|
| 120 |
+
display: inline-flex;
|
| 121 |
+
align-items: center;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
.page-input {
|
| 125 |
+
width: 50px;
|
| 126 |
+
padding: 8px 25px 8px 8px; /* Right padding gives space for label */
|
| 127 |
+
text-align: center;
|
| 128 |
+
border: 1px solid #ddd;
|
| 129 |
+
border-radius: 4px;
|
| 130 |
+
-moz-appearance: textfield; /* Hide number arrows in Firefox */
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
/* Hide number arrows in Chrome/Safari */
|
| 134 |
+
.page-input::-webkit-outer-spin-button,
|
| 135 |
+
.page-input::-webkit-inner-spin-button {
|
| 136 |
+
-webkit-appearance: none;
|
| 137 |
+
margin: 0;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
.page-input-label {
|
| 141 |
+
position: absolute;
|
| 142 |
+
right: 8px;
|
| 143 |
+
color: #666;
|
| 144 |
+
pointer-events: none; /* Allows clicking through to input */
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
/* Pagination styling */
|
| 148 |
+
.pagination-container {
|
| 149 |
+
margin: 20px 0;
|
| 150 |
+
text-align: center;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
.pagination {
|
| 154 |
+
display: inline-flex;
|
| 155 |
+
align-items: center;
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
.pagination-button {
|
| 159 |
+
padding: 8px 16px;
|
| 160 |
+
background: #4a6fa5;
|
| 161 |
+
color: white;
|
| 162 |
+
border: none;
|
| 163 |
+
border-radius: 4px;
|
| 164 |
+
cursor: pointer;
|
| 165 |
+
display: flex;
|
| 166 |
+
align-items: center;
|
| 167 |
+
gap: 5px;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
.pagination-button-text:hover {
|
| 171 |
+
background-color: #e0e0e0;
|
| 172 |
+
transform: translateY(-1px);
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
.pagination-button-text:active {
|
| 176 |
+
transform: translateY(0);
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
.text-viewer {
|
| 180 |
+
font-family: monospace;
|
| 181 |
+
white-space: pre-wrap; /* Preserve line breaks but wrap text */
|
| 182 |
+
background: #f8f8f8;
|
| 183 |
+
padding: 20px;
|
| 184 |
+
border-radius: 5px;
|
| 185 |
+
line-height: 1.5;
|
| 186 |
+
}
|
| 187 |
+
.citation {
|
| 188 |
+
background-color: rgba(0, 255, 0, 0.2);
|
| 189 |
+
padding: 2px 0;
|
| 190 |
+
}
|
| 191 |
+
.no-content {
|
| 192 |
+
color: #999;
|
| 193 |
+
font-style: italic;
|
| 194 |
+
}
|
| 195 |
+
.pagination-container-text {
|
| 196 |
+
margin: 20px 0;
|
| 197 |
+
text-align: center;
|
| 198 |
+
}
|
| 199 |
+
.pagination-button-text {
|
| 200 |
+
padding: 8px 16px;
|
| 201 |
+
background: #4a6fa5;
|
| 202 |
+
color: white;
|
| 203 |
+
border: none;
|
| 204 |
+
border-radius: 4px;
|
| 205 |
+
cursor: pointer;
|
| 206 |
+
}
|
app/frontend/templates/base.html
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
{% block title %}
|
| 7 |
+
{% endblock %}
|
| 8 |
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
| 9 |
+
<link href="/static/styles.css" rel="stylesheet">
|
| 10 |
+
{% block head_scripts %}
|
| 11 |
+
{% endblock %}
|
| 12 |
+
</head>
|
| 13 |
+
<body>
|
| 14 |
+
{% if navbar %}
|
| 15 |
+
{% with context=navbar_context %}
|
| 16 |
+
{% include navbar_path %}
|
| 17 |
+
{% endwith %}
|
| 18 |
+
{% endif %}
|
| 19 |
+
|
| 20 |
+
{% block content %}
|
| 21 |
+
{% endblock %}
|
| 22 |
+
|
| 23 |
+
{% if footer %}
|
| 24 |
+
{% with context=footer_context %}
|
| 25 |
+
{% include footer_path %}
|
| 26 |
+
{% endwith %}
|
| 27 |
+
{% endif %}
|
| 28 |
+
|
| 29 |
+
{% block body_scripts %}
|
| 30 |
+
{% endblock %}
|
| 31 |
+
</body>
|
| 32 |
+
</html>
|
app/frontend/templates/components/navbar.html
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!-- All the data is accessible via context -->
|
| 2 |
+
<div>
|
| 3 |
+
{% if context.user.role == "guest" %}
|
| 4 |
+
<p>Hello, guest!</p>
|
| 5 |
+
{% else %}
|
| 6 |
+
<p>Hello, {{ context.user.instance.email }}</p>
|
| 7 |
+
{% endif %}
|
| 8 |
+
</div>
|
app/frontend/templates/pages/chat.html
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}
|
| 4 |
+
<title>
|
| 5 |
+
The Ultimate RAG
|
| 6 |
+
</title>
|
| 7 |
+
{% endblock %}
|
| 8 |
+
|
| 9 |
+
{% block content %}
|
| 10 |
+
<div class="chat-container">
|
| 11 |
+
<div class="chat-body">
|
| 12 |
+
<div class="hero-section py-5 mb-5">
|
| 13 |
+
<div class="container text-center">
|
| 14 |
+
<h1 class="display-4 fw-bold mb-3">The Ultimate RAG</h1>
|
| 15 |
+
<p class="tagline h5 mb-4">ask anything...</p>
|
| 16 |
+
</div>
|
| 17 |
+
</div>
|
| 18 |
+
<div>
|
| 19 |
+
<form action="/new_chat" method="post">
|
| 20 |
+
<button type="submit">Add new chat</button>
|
| 21 |
+
</form>
|
| 22 |
+
</div>
|
| 23 |
+
<div class="container search-container">
|
| 24 |
+
<!-- File Upload Section -->
|
| 25 |
+
<div class="file-upload mb-4">
|
| 26 |
+
<h5 class="mb-3">Upload Documents</h5>
|
| 27 |
+
<form id="uploadForm" enctype="multipart/form-data">
|
| 28 |
+
<div class="mb-3">
|
| 29 |
+
<input class="form-control" type="file" id="fileInput" multiple>
|
| 30 |
+
</div>
|
| 31 |
+
<div id="fileList" class="file-list"></div>
|
| 32 |
+
</form>
|
| 33 |
+
</div>
|
| 34 |
+
|
| 35 |
+
<!-- Search Section -->
|
| 36 |
+
<div class="row justify-content-center">
|
| 37 |
+
<div class="col-md-12">
|
| 38 |
+
<div class="input-group mb-3">
|
| 39 |
+
<input type="text" class="form-control search-box" id="queryInput"
|
| 40 |
+
placeholder="Ask your question..." aria-label="Ask your question">
|
| 41 |
+
<button class="btn btn-primary btn-search" id="searchButton" type="button">Search</button>
|
| 42 |
+
</div>
|
| 43 |
+
</div>
|
| 44 |
+
</div>
|
| 45 |
+
</div>
|
| 46 |
+
|
| 47 |
+
<!-- Results section -->
|
| 48 |
+
<div class="container mt-5 d-none" id="results-section">
|
| 49 |
+
<div class="row justify-content-center">
|
| 50 |
+
<div class="col-md-10">
|
| 51 |
+
<div class="card shadow-sm">
|
| 52 |
+
<div class="card-body">
|
| 53 |
+
<h5 class="card-title">Results</h5>
|
| 54 |
+
<div id="results-content"></div>
|
| 55 |
+
</div>
|
| 56 |
+
</div>
|
| 57 |
+
</div>
|
| 58 |
+
</div>
|
| 59 |
+
</div>
|
| 60 |
+
</div>
|
| 61 |
+
</div>
|
| 62 |
+
{% endblock %}
|
| 63 |
+
|
| 64 |
+
{% block body_scripts %}
|
| 65 |
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
| 66 |
+
<script>
|
| 67 |
+
// File upload handling
|
| 68 |
+
const fileInput = document.getElementById('fileInput');
|
| 69 |
+
const fileList = document.getElementById('fileList');
|
| 70 |
+
let uploadedFiles = [];
|
| 71 |
+
|
| 72 |
+
fileInput.addEventListener('change', function(e) {
|
| 73 |
+
fileList.innerHTML = '';
|
| 74 |
+
uploadedFiles = Array.from(e.target.files);
|
| 75 |
+
|
| 76 |
+
uploadedFiles.forEach((file, index) => {
|
| 77 |
+
const fileItem = document.createElement('div');
|
| 78 |
+
fileItem.className = 'file-item d-flex justify-content-between align-items-center';
|
| 79 |
+
fileItem.innerHTML = `
|
| 80 |
+
<span>${file.name}</span>
|
| 81 |
+
<button class="btn btn-sm btn-outline-danger remove-file" data-index="${index}">×</button>
|
| 82 |
+
`;
|
| 83 |
+
fileList.appendChild(fileItem);
|
| 84 |
+
});
|
| 85 |
+
|
| 86 |
+
// Add event listeners to remove buttons
|
| 87 |
+
document.querySelectorAll('.remove-file').forEach(button => {
|
| 88 |
+
button.addEventListener('click', function() {
|
| 89 |
+
const index = parseInt(this.getAttribute('data-index'));
|
| 90 |
+
uploadedFiles.splice(index, 1);
|
| 91 |
+
|
| 92 |
+
// Update file input and UI
|
| 93 |
+
const dataTransfer = new DataTransfer();
|
| 94 |
+
uploadedFiles.forEach(file => dataTransfer.items.add(file));
|
| 95 |
+
fileInput.files = dataTransfer.files;
|
| 96 |
+
|
| 97 |
+
// Re-render file list
|
| 98 |
+
const event = new Event('change');
|
| 99 |
+
fileInput.dispatchEvent(event);
|
| 100 |
+
});
|
| 101 |
+
});
|
| 102 |
+
});
|
| 103 |
+
|
| 104 |
+
// Search functionality
|
| 105 |
+
document.getElementById('searchButton').addEventListener('click', async function() {
|
| 106 |
+
const query = document.getElementById('queryInput').value.trim();
|
| 107 |
+
|
| 108 |
+
if (!query) {
|
| 109 |
+
alert('Please enter a question');
|
| 110 |
+
return;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
if (uploadedFiles.length === 0) {
|
| 114 |
+
alert('Please upload at least one document');
|
| 115 |
+
return;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
// Show loading state
|
| 119 |
+
document.getElementById('results-section').classList.remove('d-none');
|
| 120 |
+
document.getElementById('results-content').innerHTML = `
|
| 121 |
+
<div class="text-center py-4">
|
| 122 |
+
<div class="spinner-border text-primary" role="status">
|
| 123 |
+
<span class="visually-hidden">Loading...</span>
|
| 124 |
+
</div>
|
| 125 |
+
<p class="mt-2">Processing your documents and question...</p>
|
| 126 |
+
</div>
|
| 127 |
+
`;
|
| 128 |
+
|
| 129 |
+
try {
|
| 130 |
+
// Prepare form data
|
| 131 |
+
const formData = new FormData();
|
| 132 |
+
|
| 133 |
+
// Append each file
|
| 134 |
+
uploadedFiles.forEach(file => {
|
| 135 |
+
formData.append('files', file); // Must use 'files' as the key
|
| 136 |
+
});
|
| 137 |
+
|
| 138 |
+
// Append the prompt
|
| 139 |
+
formData.append('prompt', query); // Must use 'prompt' as the key
|
| 140 |
+
|
| 141 |
+
// Headers will be set automatically by the browser
|
| 142 |
+
const response = await fetch('/message_with_docs/', {
|
| 143 |
+
method: 'POST',
|
| 144 |
+
body: formData
|
| 145 |
+
});
|
| 146 |
+
|
| 147 |
+
if (!response.ok) {
|
| 148 |
+
throw new Error(`HTTP error! status: ${response.status}`);
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
const data = await response.json();
|
| 152 |
+
|
| 153 |
+
// Display results
|
| 154 |
+
document.getElementById('results-content').innerHTML = `
|
| 155 |
+
<h6>Question:</h6>
|
| 156 |
+
<p class="mb-4">${query}</p>
|
| 157 |
+
<h6>Answer:</h6>
|
| 158 |
+
<div class="alert alert-success">
|
| 159 |
+
${data.response || 'No answer found in the provided documents'}
|
| 160 |
+
</div>
|
| 161 |
+
${data.sources ? `<h6>Sources:</h6><p>${data.sources}</p>` : ''}
|
| 162 |
+
`;
|
| 163 |
+
} catch (error) {
|
| 164 |
+
console.error('Error:', error);
|
| 165 |
+
document.getElementById('results-content').innerHTML = `
|
| 166 |
+
<div class="alert alert-danger">
|
| 167 |
+
Error processing your request: ${error.message}
|
| 168 |
+
</div>
|
| 169 |
+
`;
|
| 170 |
+
}
|
| 171 |
+
});
|
| 172 |
+
</script>
|
| 173 |
+
{% endblock %}
|
app/frontend/templates/pages/login.html
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}
|
| 4 |
+
<title>User Registration</title>
|
| 5 |
+
{% endblock %}
|
| 6 |
+
|
| 7 |
+
{% block content %}
|
| 8 |
+
<div class="login-body">
|
| 9 |
+
<h1>Login</h1>
|
| 10 |
+
<form id="registrationForm">
|
| 11 |
+
<div class="form-group">
|
| 12 |
+
<label class="login-label" for="email">Email:</label>
|
| 13 |
+
<input class="input-field" type="email" id="email" name="email" required>
|
| 14 |
+
<div id="emailError" class="error"></div>
|
| 15 |
+
</div>
|
| 16 |
+
<div class="form-group">
|
| 17 |
+
<label class="login-label" for="password">Password (8-32 characters):</label>
|
| 18 |
+
<input class="input-field" type="password" id="password" name="password" required minlength="8" maxlength="32">
|
| 19 |
+
<div id="passwordError" class="error"></div>
|
| 20 |
+
</div>
|
| 21 |
+
<button class="login-button" type="submit">Login</button>
|
| 22 |
+
</form>
|
| 23 |
+
</div>
|
| 24 |
+
{% endblock %}
|
| 25 |
+
|
| 26 |
+
{% block body_scripts %}
|
| 27 |
+
<script>
|
| 28 |
+
document.getElementById('registrationForm').addEventListener('submit', async function(e) {
|
| 29 |
+
e.preventDefault();
|
| 30 |
+
|
| 31 |
+
// Clear previous errors
|
| 32 |
+
document.getElementById('emailError').textContent = '';
|
| 33 |
+
document.getElementById('passwordError').textContent = '';
|
| 34 |
+
|
| 35 |
+
const email = document.getElementById('email').value;
|
| 36 |
+
const password = document.getElementById('password').value;
|
| 37 |
+
|
| 38 |
+
try {
|
| 39 |
+
const response = await fetch('/login', {
|
| 40 |
+
method: 'POST',
|
| 41 |
+
headers: {
|
| 42 |
+
'Content-Type': 'application/json',
|
| 43 |
+
},
|
| 44 |
+
body: JSON.stringify({
|
| 45 |
+
email: email,
|
| 46 |
+
password: password
|
| 47 |
+
})
|
| 48 |
+
});
|
| 49 |
+
|
| 50 |
+
const data = await response.json();
|
| 51 |
+
|
| 52 |
+
if (!response.ok) {
|
| 53 |
+
// Handle validation errors from backend
|
| 54 |
+
if (data.detail) {
|
| 55 |
+
if (Array.isArray(data.detail)) {
|
| 56 |
+
data.detail.forEach(error => {
|
| 57 |
+
if (error.loc && error.loc.includes('email')) {
|
| 58 |
+
document.getElementById('emailError').textContent = error.msg;
|
| 59 |
+
}
|
| 60 |
+
if (error.loc && error.loc.includes('password')) {
|
| 61 |
+
document.getElementById('passwordError').textContent = error.msg;
|
| 62 |
+
}
|
| 63 |
+
});
|
| 64 |
+
} else {
|
| 65 |
+
// Handle single error message
|
| 66 |
+
alert(data.detail);
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
+
return;
|
| 70 |
+
}
|
| 71 |
+
alert('User registered successfully!');
|
| 72 |
+
|
| 73 |
+
} catch (error) {
|
| 74 |
+
console.error('Error:', error);
|
| 75 |
+
alert('An error occurred during registration');
|
| 76 |
+
}
|
| 77 |
+
});
|
| 78 |
+
</script>
|
| 79 |
+
{% endblock %}
|
app/frontend/templates/pages/main.html
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}
|
| 4 |
+
<title>Welcome</title>
|
| 5 |
+
{% endblock %}
|
| 6 |
+
|
| 7 |
+
{% block content %}
|
| 8 |
+
<button onclick="location.href='/new_chat'" formmethod="post">Get started!</button>
|
| 9 |
+
{% endblock %}
|
app/frontend/templates/pages/registration.html
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}
|
| 4 |
+
<title>User Registration</title>
|
| 5 |
+
{% endblock %}
|
| 6 |
+
|
| 7 |
+
{% block content %}
|
| 8 |
+
<div class="login-body">
|
| 9 |
+
<h1>Register New User</h1>
|
| 10 |
+
<form id="registrationForm">
|
| 11 |
+
<div class="form-group">
|
| 12 |
+
<label class="login-label" for="email">Email:</label>
|
| 13 |
+
<input class="input-field" type="email" id="email" name="email" required>
|
| 14 |
+
<div id="emailError" class="error"></div>
|
| 15 |
+
</div>
|
| 16 |
+
<div class="form-group">
|
| 17 |
+
<label class="login-label" for="password">Password (8-32 characters):</label>
|
| 18 |
+
<input class="input-field" type="password" id="password" name="password" required minlength="8" maxlength="32">
|
| 19 |
+
<div id="passwordError" class="error"></div>
|
| 20 |
+
</div>
|
| 21 |
+
<button class="login-button" type="submit">Register</button>
|
| 22 |
+
</form>
|
| 23 |
+
</div>
|
| 24 |
+
{% endblock %}
|
| 25 |
+
|
| 26 |
+
{% block body_scripts %}
|
| 27 |
+
<script>
|
| 28 |
+
document.getElementById('registrationForm').addEventListener('submit', async function(e) {
|
| 29 |
+
e.preventDefault();
|
| 30 |
+
|
| 31 |
+
// Clear previous errors
|
| 32 |
+
document.getElementById('emailError').textContent = '';
|
| 33 |
+
document.getElementById('passwordError').textContent = '';
|
| 34 |
+
|
| 35 |
+
const email = document.getElementById('email').value;
|
| 36 |
+
const password = document.getElementById('password').value;
|
| 37 |
+
|
| 38 |
+
try {
|
| 39 |
+
const response = await fetch('/new_user', {
|
| 40 |
+
method: 'POST',
|
| 41 |
+
headers: {
|
| 42 |
+
'Content-Type': 'application/json',
|
| 43 |
+
},
|
| 44 |
+
body: JSON.stringify({
|
| 45 |
+
email: email,
|
| 46 |
+
password: password
|
| 47 |
+
})
|
| 48 |
+
});
|
| 49 |
+
|
| 50 |
+
const data = await response.json();
|
| 51 |
+
|
| 52 |
+
if (!response.ok) {
|
| 53 |
+
// Handle validation errors from backend
|
| 54 |
+
if (data.detail) {
|
| 55 |
+
if (Array.isArray(data.detail)) {
|
| 56 |
+
data.detail.forEach(error => {
|
| 57 |
+
if (error.loc && error.loc.includes('email')) {
|
| 58 |
+
document.getElementById('emailError').textContent = error.msg;
|
| 59 |
+
}
|
| 60 |
+
if (error.loc && error.loc.includes('password')) {
|
| 61 |
+
document.getElementById('passwordError').textContent = error.msg;
|
| 62 |
+
}
|
| 63 |
+
});
|
| 64 |
+
} else {
|
| 65 |
+
// Handle single error message
|
| 66 |
+
alert(data.detail);
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
+
return;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
// Registration successful
|
| 73 |
+
alert('User registered successfully!');
|
| 74 |
+
// Optionally redirect to login page or other page
|
| 75 |
+
// window.location.href = '/login';
|
| 76 |
+
|
| 77 |
+
} catch (error) {
|
| 78 |
+
console.error('Error:', error);
|
| 79 |
+
alert('An error occurred during registration');
|
| 80 |
+
}
|
| 81 |
+
});
|
| 82 |
+
</script>
|
| 83 |
+
{% endblock %}
|
app/frontend/templates/pages/show_pdf.html
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}
|
| 4 |
+
<title>PDF Viewer</title>
|
| 5 |
+
{% endblock %}
|
| 6 |
+
|
| 7 |
+
{% block head_scripts %}
|
| 8 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.10.377/pdf.min.js"></script>
|
| 9 |
+
{% endblock %}
|
| 10 |
+
|
| 11 |
+
{% block content %}
|
| 12 |
+
<div class="pagination-container">
|
| 13 |
+
<div class="pagination">
|
| 14 |
+
<button id="prev" class="pagination-button">
|
| 15 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
| 16 |
+
<path fill-rule="evenodd" d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z"/>
|
| 17 |
+
</svg>
|
| 18 |
+
Previous
|
| 19 |
+
</button>
|
| 20 |
+
|
| 21 |
+
<div class="page-input-container">
|
| 22 |
+
<input type="number" id="pageNum" value="{{ page }}" class="page-input" style="padding-right: 30px;">
|
| 23 |
+
<span class="page-input-label">of {{ total_pages }}</span>
|
| 24 |
+
</div>
|
| 25 |
+
|
| 26 |
+
<button id="next" class="pagination-button">
|
| 27 |
+
Next
|
| 28 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
| 29 |
+
<path fill-rule="evenodd" d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"/>
|
| 30 |
+
</svg>
|
| 31 |
+
</button>
|
| 32 |
+
</div>
|
| 33 |
+
</div>
|
| 34 |
+
|
| 35 |
+
<div id="pdf-container">
|
| 36 |
+
<canvas id="pdf-canvas"></canvas>
|
| 37 |
+
</div>
|
| 38 |
+
{% endblock %}
|
| 39 |
+
|
| 40 |
+
{% block body_scripts %}
|
| 41 |
+
<script>
|
| 42 |
+
pdfjsLib = window['pdfjs-dist/build/pdf'];
|
| 43 |
+
pdfjsLib.GlobalWorkerOptions.workerSrc =
|
| 44 |
+
'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.10.377/pdf.worker.min.js';
|
| 45 |
+
|
| 46 |
+
let pdfDoc = null;
|
| 47 |
+
let currentPage = {{ page }};
|
| 48 |
+
const urlPath = "{{ url_path }}";
|
| 49 |
+
|
| 50 |
+
pdfjsLib.getDocument(urlPath).promise.then(function(pdf) {
|
| 51 |
+
pdfDoc = pdf;
|
| 52 |
+
document.getElementById('pageNum').max = pdf.numPages;
|
| 53 |
+
document.querySelector('.page-input-label').textContent = `of ${pdf.numPages}`;
|
| 54 |
+
renderPage(currentPage);
|
| 55 |
+
});
|
| 56 |
+
|
| 57 |
+
function renderPage(num) {
|
| 58 |
+
pdfDoc.getPage(num).then(function(page) {
|
| 59 |
+
const scale = 1.5;
|
| 60 |
+
const viewport = page.getViewport({ scale });
|
| 61 |
+
const canvas = document.getElementById('pdf-canvas');
|
| 62 |
+
const ctx = canvas.getContext('2d');
|
| 63 |
+
|
| 64 |
+
// Set canvas dimensions
|
| 65 |
+
canvas.height = viewport.height;
|
| 66 |
+
canvas.width = viewport.width;
|
| 67 |
+
|
| 68 |
+
// Render PDF page
|
| 69 |
+
page.render({
|
| 70 |
+
canvasContext: ctx,
|
| 71 |
+
viewport: viewport
|
| 72 |
+
});
|
| 73 |
+
});
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
// Navigation controls
|
| 77 |
+
document.getElementById('prev').addEventListener('click', function() {
|
| 78 |
+
if (currentPage <= 1) return;
|
| 79 |
+
currentPage--;
|
| 80 |
+
document.getElementById('pageNum').value = currentPage;
|
| 81 |
+
renderPage(currentPage);
|
| 82 |
+
});
|
| 83 |
+
|
| 84 |
+
document.getElementById('next').addEventListener('click', function() {
|
| 85 |
+
if (currentPage >= pdfDoc.numPages) return;
|
| 86 |
+
currentPage++;
|
| 87 |
+
document.getElementById('pageNum').value = currentPage;
|
| 88 |
+
renderPage(currentPage);
|
| 89 |
+
});
|
| 90 |
+
|
| 91 |
+
document.getElementById('pageNum').addEventListener('change', function() {
|
| 92 |
+
const newPage = Math.min(Math.max(1, parseInt(this.value)), pdfDoc.numPages);
|
| 93 |
+
currentPage = newPage;
|
| 94 |
+
this.value = currentPage;
|
| 95 |
+
renderPage(currentPage);
|
| 96 |
+
});
|
| 97 |
+
</script>
|
| 98 |
+
{% endblock %}
|
app/frontend/templates/pages/show_text.html
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}
|
| 4 |
+
<title>Text Viewer</title>
|
| 5 |
+
{% endblock %}
|
| 6 |
+
|
| 7 |
+
{% block content %}
|
| 8 |
+
<div class="pagination-container-text">
|
| 9 |
+
<div class="pagination-text">
|
| 10 |
+
<button id="prev" class="pagination-button-text" onclick="location.href='#anchor'">
|
| 11 |
+
Look at the citation
|
| 12 |
+
</button>
|
| 13 |
+
</div>
|
| 14 |
+
</div>
|
| 15 |
+
|
| 16 |
+
<div class="text-viewer">
|
| 17 |
+
{% if text_before_citation %}
|
| 18 |
+
{% for line in text_before_citation -%}
|
| 19 |
+
<div>{{ line }}</div>
|
| 20 |
+
{%- endfor %}
|
| 21 |
+
{% else %}
|
| 22 |
+
<span class="no-content">No text available</span>
|
| 23 |
+
{% endif %}
|
| 24 |
+
|
| 25 |
+
{% if anchor_added %}
|
| 26 |
+
<a id="anchor"></a>
|
| 27 |
+
{% endif %}
|
| 28 |
+
|
| 29 |
+
{% if citation %}
|
| 30 |
+
<div class="citation">
|
| 31 |
+
{% for line in citation -%}
|
| 32 |
+
<div>{{ line }}</div>
|
| 33 |
+
{%- endfor %}
|
| 34 |
+
</div>
|
| 35 |
+
{% else %}
|
| 36 |
+
<span class="no-content">No text available</span>
|
| 37 |
+
{% endif %}
|
| 38 |
+
|
| 39 |
+
{% if text_after_citation %}
|
| 40 |
+
{% for line in text_after_citation -%}
|
| 41 |
+
<div>{{ line }}</div>
|
| 42 |
+
{%- endfor %}
|
| 43 |
+
{% else %}
|
| 44 |
+
<span class="no-content">No text available</span>
|
| 45 |
+
{% endif %}
|
| 46 |
+
</div>
|
| 47 |
+
{% endblock %}
|
app/main.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.settings import api_config, base_path
|
| 2 |
+
import uvicorn
|
| 3 |
+
import os
|
| 4 |
+
from app.backend.models.db_service import automigrate
|
| 5 |
+
|
| 6 |
+
def initialize_system() -> bool:
|
| 7 |
+
success = True
|
| 8 |
+
path = os.path.dirname(base_path)
|
| 9 |
+
temp_storage_path = os.path.join(path, os.path.join("app", "temp_storage"))
|
| 10 |
+
temp_storage_path_pdf = os.path.join(path, os.path.join("app", "temp_storage", "pdfs"))
|
| 11 |
+
database_path = os.path.join(path, "database")
|
| 12 |
+
|
| 13 |
+
try:
|
| 14 |
+
os.makedirs(temp_storage_path, exist_ok=True)
|
| 15 |
+
os.makedirs(database_path, exist_ok=True)
|
| 16 |
+
os.makedirs(temp_storage_path_pdf, exist_ok=True)
|
| 17 |
+
except Exception:
|
| 18 |
+
success = False
|
| 19 |
+
print("Not all required directories were initialized")
|
| 20 |
+
|
| 21 |
+
try:
|
| 22 |
+
# os.system(f"pip install -r {os.path.join(base_path, 'requirements.txt')}")
|
| 23 |
+
pass
|
| 24 |
+
except Exception:
|
| 25 |
+
success = False
|
| 26 |
+
print("Not all package were downloaded")
|
| 27 |
+
|
| 28 |
+
return success
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def main():
|
| 32 |
+
if not initialize_system():
|
| 33 |
+
return
|
| 34 |
+
automigrate() # Note: it will drop all existing dbs and create a new ones
|
| 35 |
+
uvicorn.run(**api_config)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
if __name__ == '__main__':
|
| 39 |
+
|
| 40 |
+
# ATTENTION: run from base dir ---> python -m app.main
|
| 41 |
+
main()
|
app/models.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sentence_transformers import SentenceTransformer, \
|
| 2 |
+
CrossEncoder # SentenceTransformer -> model for embeddings, CrossEncoder -> re-ranker
|
| 3 |
+
from ctransformers import AutoModelForCausalLM
|
| 4 |
+
from torch import Tensor
|
| 5 |
+
from google import genai
|
| 6 |
+
from google.genai import types
|
| 7 |
+
from app.chunks import Chunk
|
| 8 |
+
import numpy as np # used only for type hints
|
| 9 |
+
from app.settings import device, local_llm_config, local_generation_config, gemini_generation_config
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class Embedder:
|
| 13 |
+
def __init__(self, model: str = "BAAI/bge-m3"):
|
| 14 |
+
self.device: str = device
|
| 15 |
+
self.model_name: str = model
|
| 16 |
+
self.model: SentenceTransformer = SentenceTransformer(model, device=self.device)
|
| 17 |
+
|
| 18 |
+
'''
|
| 19 |
+
Encodes string to dense vector
|
| 20 |
+
'''
|
| 21 |
+
|
| 22 |
+
def encode(self, text: str | list[str]) -> Tensor | list[Tensor]:
|
| 23 |
+
return self.model.encode(sentences=text, show_progress_bar=False, batch_size=32)
|
| 24 |
+
|
| 25 |
+
'''
|
| 26 |
+
Returns the dimensionality of dense vector
|
| 27 |
+
'''
|
| 28 |
+
|
| 29 |
+
def get_vector_dimensionality(self) -> (int | None):
|
| 30 |
+
return self.model.get_sentence_embedding_dimension()
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class Reranker:
|
| 34 |
+
def __init__(self, model: str = "cross-encoder/ms-marco-MiniLM-L6-v2"):
|
| 35 |
+
self.device: str = device
|
| 36 |
+
self.model_name: str = model
|
| 37 |
+
self.model: CrossEncoder = CrossEncoder(model, device=self.device)
|
| 38 |
+
|
| 39 |
+
'''
|
| 40 |
+
Returns re-sorted (by relevance) vector with dicts, from which we need only the 'corpus_id'
|
| 41 |
+
since it is a position of chunk in original list
|
| 42 |
+
'''
|
| 43 |
+
|
| 44 |
+
def rank(self, query: str, chunks: list[Chunk]) -> list[dict[str, int]]:
|
| 45 |
+
return self.model.rank(query, [chunk.get_raw_text() for chunk in chunks])
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
# TODO: add models parameters to global config file
|
| 49 |
+
# TODO: add exception handling when response have more tokens than was set
|
| 50 |
+
# TODO: find a way to restrict the model for providing too long answers
|
| 51 |
+
|
| 52 |
+
class LocalLLM:
|
| 53 |
+
def __init__(self):
|
| 54 |
+
self.model = AutoModelForCausalLM.from_pretrained(**local_llm_config)
|
| 55 |
+
|
| 56 |
+
'''
|
| 57 |
+
Produces the response to user's prompt
|
| 58 |
+
|
| 59 |
+
stream -> flag, determines weather we need to wait until the response is ready or can show it token by token
|
| 60 |
+
|
| 61 |
+
TODO: invent a way to really stream the answer (as return value)
|
| 62 |
+
'''
|
| 63 |
+
|
| 64 |
+
def get_response(self, prompt: str, stream: bool = True, logging: bool = True,
|
| 65 |
+
use_default_config: bool = True) -> str:
|
| 66 |
+
|
| 67 |
+
with open("prompt.txt", "w") as f:
|
| 68 |
+
f.write(prompt)
|
| 69 |
+
|
| 70 |
+
generated_text = ""
|
| 71 |
+
tokenized_text: list[int] = self.model.tokenize(text=prompt)
|
| 72 |
+
response: list[int] = self.model.generate(tokens=tokenized_text, **local_generation_config)
|
| 73 |
+
|
| 74 |
+
if logging:
|
| 75 |
+
print(response)
|
| 76 |
+
|
| 77 |
+
if not stream:
|
| 78 |
+
return self.model.detokenize(response)
|
| 79 |
+
|
| 80 |
+
for token in response:
|
| 81 |
+
chunk = self.model.detokenize([token])
|
| 82 |
+
generated_text += chunk
|
| 83 |
+
if logging:
|
| 84 |
+
print(chunk, end="", flush=True) # flush -> clear the buffer
|
| 85 |
+
|
| 86 |
+
return generated_text
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
class Gemini:
|
| 90 |
+
def __init__(self, model="gemini-2.0-flash"):
|
| 91 |
+
self.client = genai.Client(api_key=os.environ['GEMINI_API_KEY'])
|
| 92 |
+
self.model = model
|
| 93 |
+
|
| 94 |
+
def get_response(self, prompt: str, stream: bool = True, logging: bool = True,
|
| 95 |
+
use_default_config: bool = False) -> str:
|
| 96 |
+
with open("prompt.txt", "w", encoding="utf-8", errors="replace") as f:
|
| 97 |
+
f.write(prompt)
|
| 98 |
+
|
| 99 |
+
response = self.client.models.generate_content(
|
| 100 |
+
model=self.model,
|
| 101 |
+
contents=prompt,
|
| 102 |
+
config=types.GenerateContentConfig(**gemini_generation_config) if use_default_config else None
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
return response.text
|
app/processor.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langchain_community.document_loaders import PyPDFLoader, UnstructuredWordDocumentLoader, TextLoader
|
| 2 |
+
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
| 3 |
+
from langchain_core.documents import Document
|
| 4 |
+
from app.models import Embedder
|
| 5 |
+
from app.chunks import Chunk
|
| 6 |
+
import nltk # used for proper tokenizer workflow
|
| 7 |
+
from uuid import uuid4 # for generating unique id as hex (uuid4 is used as it generates ids form pseudo random numbers unlike uuid1 and others)
|
| 8 |
+
import numpy as np
|
| 9 |
+
from app.settings import logging, text_splitter_config, embedder_model
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
# TODO: replace PDFloader since it is completely unusable OR try to fix it
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class DocumentProcessor:
|
| 16 |
+
'''
|
| 17 |
+
TODO: determine the most suitable chunk size
|
| 18 |
+
|
| 19 |
+
chunks -> the list of chunks from loaded files
|
| 20 |
+
chunks_unsaved -> the list of recently added chunks that have not been saved to db yet
|
| 21 |
+
processed -> the list of files that were already splitted into chunks
|
| 22 |
+
upprocessed -> !processed
|
| 23 |
+
text_splitter -> text splitting strategy
|
| 24 |
+
'''
|
| 25 |
+
|
| 26 |
+
def __init__(self):
|
| 27 |
+
self.chunks: list[Chunk] = []
|
| 28 |
+
self.chunks_unsaved: list[Chunk] = []
|
| 29 |
+
self.processed: list[Document] = []
|
| 30 |
+
self.unprocessed: list[Document] = []
|
| 31 |
+
self.embedder = Embedder(embedder_model)
|
| 32 |
+
self.text_splitter = RecursiveCharacterTextSplitter(**text_splitter_config)
|
| 33 |
+
|
| 34 |
+
'''
|
| 35 |
+
Measures cosine between two vectors
|
| 36 |
+
'''
|
| 37 |
+
def cosine_similarity(self, vec1, vec2):
|
| 38 |
+
return vec1 @ vec2 / (np.linalg.norm(vec1) * np.linalg.norm(vec2))
|
| 39 |
+
|
| 40 |
+
'''
|
| 41 |
+
Updates a list of the most relevant chunks without interacting with db
|
| 42 |
+
'''
|
| 43 |
+
def update_most_relevant_chunk(self, chunk: list[np.float64, Chunk], relevant_chunks: list[list[np.float64, Chunk]],
|
| 44 |
+
mx_len=15):
|
| 45 |
+
relevant_chunks.append(chunk)
|
| 46 |
+
for i in range(len(relevant_chunks) - 1, 0, -1):
|
| 47 |
+
if relevant_chunks[i][0] > relevant_chunks[i - 1][0]:
|
| 48 |
+
relevant_chunks[i], relevant_chunks[i - 1] = relevant_chunks[i - 1], relevant_chunks[i]
|
| 49 |
+
else:
|
| 50 |
+
break
|
| 51 |
+
|
| 52 |
+
if len(relevant_chunks) > mx_len:
|
| 53 |
+
del relevant_chunks[-1]
|
| 54 |
+
|
| 55 |
+
'''
|
| 56 |
+
Loads one file - extracts text from file
|
| 57 |
+
|
| 58 |
+
TODO: Replace UnstructuredWordDocumentLoader with Docx2txtLoader
|
| 59 |
+
TODO: Play with .pdf and text from img extraction
|
| 60 |
+
TODO: Try chunking with llm
|
| 61 |
+
|
| 62 |
+
add_to_unprocessed -> used to add loaded file to the list of unprocessed(unchunked) files if true
|
| 63 |
+
'''
|
| 64 |
+
def load_document(self, filepath: str, add_to_unprocessed: bool = False) -> list[Document]:
|
| 65 |
+
loader = None
|
| 66 |
+
|
| 67 |
+
if filepath.endswith(".pdf"):
|
| 68 |
+
loader = PyPDFLoader(
|
| 69 |
+
file_path=filepath) # splits each presentation into slides and processes it as separate file
|
| 70 |
+
elif filepath.endswith(".docx") or filepath.endswith(".doc"):
|
| 71 |
+
# loader = Docx2txtLoader(file_path=filepath) ## try it later, since UnstructuredWordDocumentLoader is extremly slow
|
| 72 |
+
loader = UnstructuredWordDocumentLoader(file_path=filepath)
|
| 73 |
+
elif filepath.endswith(".txt"):
|
| 74 |
+
loader = TextLoader(file_path=filepath)
|
| 75 |
+
|
| 76 |
+
if loader is None:
|
| 77 |
+
raise RuntimeError("Unsupported type of file")
|
| 78 |
+
|
| 79 |
+
documents: list[
|
| 80 |
+
Document] = [] # We can not assign a single value to the document since .pdf are splitted into several files
|
| 81 |
+
try:
|
| 82 |
+
documents = loader.load()
|
| 83 |
+
except Exception:
|
| 84 |
+
raise RuntimeError("File is corrupted")
|
| 85 |
+
|
| 86 |
+
if add_to_unprocessed:
|
| 87 |
+
for doc in documents:
|
| 88 |
+
self.unprocessed.append(doc)
|
| 89 |
+
|
| 90 |
+
return documents
|
| 91 |
+
|
| 92 |
+
'''
|
| 93 |
+
Similar to load_document, but for multiple files
|
| 94 |
+
|
| 95 |
+
add_to_unprocessed -> used to add loaded files to the list of unprocessed(unchunked) files if true
|
| 96 |
+
'''
|
| 97 |
+
def load_documents(self, documents: list[str], add_to_unprocessed: bool = False) -> list[Document]:
|
| 98 |
+
extracted_documents: list[Document] = []
|
| 99 |
+
|
| 100 |
+
for doc in documents:
|
| 101 |
+
temp_storage: list[Document] = []
|
| 102 |
+
|
| 103 |
+
try:
|
| 104 |
+
temp_storage = self.load_document(filepath=doc,
|
| 105 |
+
add_to_unprocessed=False) # In some cases it should be True, but i can not imagine any :(
|
| 106 |
+
except Exception as e:
|
| 107 |
+
logging.error("Error at load_documents while loading %s", doc, exc_info=e)
|
| 108 |
+
continue
|
| 109 |
+
|
| 110 |
+
for extrc_doc in temp_storage:
|
| 111 |
+
extracted_documents.append(extrc_doc)
|
| 112 |
+
|
| 113 |
+
if add_to_unprocessed:
|
| 114 |
+
self.unprocessed.append(extrc_doc)
|
| 115 |
+
|
| 116 |
+
return extracted_documents
|
| 117 |
+
|
| 118 |
+
'''
|
| 119 |
+
Generates chunks with recursive splitter from the list of unprocessed files, add files to the list of processed, and clears unprocessed
|
| 120 |
+
|
| 121 |
+
TODO: try to split text with other llm (not really needed, but we should at least try it)
|
| 122 |
+
'''
|
| 123 |
+
def generate_chunks(self, query: str = "", embedding: bool = False):
|
| 124 |
+
most_relevant = []
|
| 125 |
+
|
| 126 |
+
if embedding:
|
| 127 |
+
query_embedded = self.embedder.encode(query)
|
| 128 |
+
|
| 129 |
+
for document in self.unprocessed:
|
| 130 |
+
self.processed.append(document)
|
| 131 |
+
|
| 132 |
+
text: list[str] = self.text_splitter.split_documents([document])
|
| 133 |
+
lines: list[str] = document.page_content.split("\n")
|
| 134 |
+
|
| 135 |
+
for chunk in text:
|
| 136 |
+
|
| 137 |
+
start_l, end_l = self.get_start_end_lines(
|
| 138 |
+
splitted_text=lines,
|
| 139 |
+
start_char=chunk.metadata.get("start_index", 0),
|
| 140 |
+
end_char=chunk.metadata.get("start_index", 0) + len(chunk.page_content)
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
newChunk = Chunk(
|
| 144 |
+
id=uuid4(),
|
| 145 |
+
filename=document.metadata.get("source", ""),
|
| 146 |
+
page_number=document.metadata.get("page", 0),
|
| 147 |
+
start_index=chunk.metadata.get("start_index", 0),
|
| 148 |
+
start_line=start_l,
|
| 149 |
+
end_line=end_l,
|
| 150 |
+
text=chunk.page_content
|
| 151 |
+
)
|
| 152 |
+
|
| 153 |
+
if embedding:
|
| 154 |
+
chunk_embedded = self.embedder.encode(newChunk.text)
|
| 155 |
+
similarity = self.cosine_similarity(query_embedded, chunk_embedded)
|
| 156 |
+
self.update_most_relevant_chunk([similarity, newChunk], most_relevant)
|
| 157 |
+
|
| 158 |
+
self.chunks.append(newChunk)
|
| 159 |
+
self.chunks_unsaved.append(newChunk)
|
| 160 |
+
|
| 161 |
+
self.unprocessed = []
|
| 162 |
+
print(len(self.chunks_unsaved))
|
| 163 |
+
return most_relevant
|
| 164 |
+
|
| 165 |
+
'''
|
| 166 |
+
Determines the line, were the chunk starts and ends (1-based indexing)
|
| 167 |
+
|
| 168 |
+
Some magic stuff here. To be honest, i understood it after 7th attempt
|
| 169 |
+
|
| 170 |
+
TODO: invent more efficient way
|
| 171 |
+
|
| 172 |
+
splitted_text -> original text splitted by \n
|
| 173 |
+
start_char -> index of symbol, were current chunk starts
|
| 174 |
+
end_char -> index of symbol, were current chunk ends
|
| 175 |
+
debug_mode -> flag, which enables printing useful info about the process
|
| 176 |
+
'''
|
| 177 |
+
def get_start_end_lines(self, splitted_text: list[str], start_char: int, end_char: int, debug_mode: bool = False) -> \
|
| 178 |
+
tuple[int, int]:
|
| 179 |
+
if debug_mode:
|
| 180 |
+
logging.info(splitted_text)
|
| 181 |
+
|
| 182 |
+
start, end, char_ct = 0, 0, 0
|
| 183 |
+
iter_count = 1
|
| 184 |
+
|
| 185 |
+
for i, line in enumerate(splitted_text):
|
| 186 |
+
if debug_mode:
|
| 187 |
+
logging.info(
|
| 188 |
+
f"start={start_char}, current={char_ct}, end_current={char_ct + len(line) + 1}, end={end_char}, len={len(line)}, iter={iter_count}\n")
|
| 189 |
+
|
| 190 |
+
if char_ct <= start_char <= char_ct + len(line) + 1:
|
| 191 |
+
start = i + 1
|
| 192 |
+
if char_ct <= end_char <= char_ct + len(line) + 1:
|
| 193 |
+
end = i + 1
|
| 194 |
+
break
|
| 195 |
+
|
| 196 |
+
iter_count += 1
|
| 197 |
+
char_ct += len(line) + 1
|
| 198 |
+
|
| 199 |
+
if debug_mode:
|
| 200 |
+
logging.info(f"result => {start} {end}\n\n\n")
|
| 201 |
+
|
| 202 |
+
return start, end
|
| 203 |
+
|
| 204 |
+
'''
|
| 205 |
+
Note: it should be used only once to download tokenizers, futher usage is not recommended
|
| 206 |
+
'''
|
| 207 |
+
def update_nltk(self) -> None:
|
| 208 |
+
nltk.download('punkt')
|
| 209 |
+
nltk.download('averaged_perceptron_tagger')
|
| 210 |
+
|
| 211 |
+
'''
|
| 212 |
+
For now the system works as follows: we save recently loaded chunks in two arrays:
|
| 213 |
+
chunks - for all chunks, even for that ones that havn't been saveed to db
|
| 214 |
+
chunks_unsaved - for chunks that have been added recently
|
| 215 |
+
I do not know weather we really need to store all chunks that were added in the
|
| 216 |
+
current session, but chunks_unsaved are used to avoid dublications while saving to db.
|
| 217 |
+
'''
|
| 218 |
+
def clear_unsaved_chunks(self):
|
| 219 |
+
self.chunks_unsaved = []
|
| 220 |
+
|
| 221 |
+
def get_all_chunks(self) -> list[Chunk]:
|
| 222 |
+
return self.chunks
|
| 223 |
+
|
| 224 |
+
'''
|
| 225 |
+
If we want to save chunks to db, we need to clear the temp storage to avoid dublications
|
| 226 |
+
'''
|
| 227 |
+
def get_and_save_unsaved_chunks(self) -> list[Chunk]:
|
| 228 |
+
chunks_copy: list[Chunk] = self.chunks.copy()
|
| 229 |
+
self.clear_unsaved_chunks()
|
| 230 |
+
return chunks_copy
|
app/prompt.txt
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
INITIAL_QUERY: Here are some sources located at section CONTEXT_DOCUMENTS. Read these carefully, as you will be asked a Query about them.
|
| 2 |
+
|
| 3 |
+
# General Instructions
|
| 4 |
+
|
| 5 |
+
You are an expert information retrieval assistant. Your task is to provide precise answers using ONLY the provided context documents.
|
| 6 |
+
|
| 7 |
+
Base answers SOLELY on provided context.
|
| 8 |
+
|
| 9 |
+
Write an accurate, detailed, and comprehensive response to the user's query located at QUESTION. Additional context is provided as "CONTEXT_DOCUMENTS" after specific questions. Your answer should be informed by the provided "Search results". Your answer must be precise, of high-quality, and written by an expert using an unbiased and journalistic tone. Your answer must be written in the same language as the query, even if language preference is different.
|
| 10 |
+
|
| 11 |
+
You MUST cite the most relevant search results that answer the query. Do not mention any irrelevant results. You MUST ADHERE to the following instructions for citing search results:
|
| 12 |
+
- For every fact/quote, use: `[relevant text excerpt] [Source: {filename}, Page: {page_number}, Lines: {start_line}-{end_line}, Start: {start_index}]`. For example, `Water can be freezed and turned into ice. [Source: 'home/general_info.txt, Page: 12, Lines: 22-23, Start: 2890]`
|
| 13 |
+
- ALWAYS use brackets. Only use this format to cite search results. NEVER include a References section at the end of your answer. Insert citations IMMEDIATELY after quoted text.
|
| 14 |
+
- If you don't know the answer or the premise is incorrect, explain why.
|
| 15 |
+
- DO NOT change any part of reference.
|
| 16 |
+
If the search results are empty or unhelpful, answer the query as well as you can with existing knowledge.
|
| 17 |
+
|
| 18 |
+
Cross-check all facts against multiple sources where available
|
| 19 |
+
|
| 20 |
+
You MUST NEVER use moralization or hedging language. AVOID using the following phrases:
|
| 21 |
+
- "It is important to ..."
|
| 22 |
+
- "It is inappropriate ..."
|
| 23 |
+
- "It is subjective ..."
|
| 24 |
+
|
| 25 |
+
You MUST ADHERE to the following formatting instructions:
|
| 26 |
+
- Use markdown to format paragraphs, lists, tables, and quotes whenever possible.
|
| 27 |
+
- Use headings level 2 and 3 to separate sections of your response, like "## Header", but NEVER start an answer with a heading or title of any kind.
|
| 28 |
+
- Use single new lines for lists and double new lines for paragraphs.
|
| 29 |
+
- Use markdown to render images given in the search results.
|
| 30 |
+
- NEVER write URLs or links.
|
| 31 |
+
|
| 32 |
+
# Query type specifications
|
| 33 |
+
|
| 34 |
+
You must use different instructions to write your answer based on the type of the user's query. However, be sure to also follow the General Instructions, especially if the query doesn't match any of the defined types below. Here are the supported types.
|
| 35 |
+
|
| 36 |
+
## Academic Research
|
| 37 |
+
|
| 38 |
+
You must provide long and detailed answers for academic research queries. Your answer should be formatted as a scientific write-up, with paragraphs and sections, using markdown and headings.
|
| 39 |
+
|
| 40 |
+
## Recent News
|
| 41 |
+
|
| 42 |
+
You need to concisely summarize recent news events based on the provided search results, grouping them by topics. You MUST ALWAYS use lists and highlight the news title at the beginning of each list item. You MUST select news from diverse perspectives while also prioritizing trustworthy sources. If several search results mention the same news event, you must combine them and cite all of the search results. Prioritize more recent events, ensuring to compare timestamps. You MUST NEVER start your answer with a heading of any kind.
|
| 43 |
+
|
| 44 |
+
## Weather
|
| 45 |
+
|
| 46 |
+
Your answer should be very short and only provide the weather forecast. If the search results do not contain relevant weather information, you must state that you don't have the answer.
|
| 47 |
+
|
| 48 |
+
## People
|
| 49 |
+
|
| 50 |
+
You need to write a short biography for the person mentioned in the query. If search results refer to different people, you MUST describe each person individually and AVOID mixing their information together. NEVER start your answer with the person's name as a header.
|
| 51 |
+
|
| 52 |
+
## Coding
|
| 53 |
+
|
| 54 |
+
You MUST use markdown code blocks to write code, specifying the language for syntax highlighting, for example ```bash or ```python If the user's query asks for code, you should write the code first and then explain it.
|
| 55 |
+
|
| 56 |
+
## Cooking Recipes
|
| 57 |
+
|
| 58 |
+
You need to provide step-by-step cooking recipes, clearly specifying the ingredient, the amount, and precise instructions during each step.
|
| 59 |
+
|
| 60 |
+
## Translation
|
| 61 |
+
|
| 62 |
+
If a user asks you to translate something, you must not cite any search results and should just provide the translation.
|
| 63 |
+
|
| 64 |
+
## Creative Writing
|
| 65 |
+
|
| 66 |
+
If the query requires creative writing, you DO NOT need to use or cite search results, and you may ignore General Instructions pertaining only to search. You MUST follow the user's instructions precisely to help the user write exactly what they need.
|
| 67 |
+
|
| 68 |
+
## Science and Math
|
| 69 |
+
|
| 70 |
+
If the user query is about some simple calculation, only answer with the final result. Follow these rules for writing formulas:
|
| 71 |
+
- Always use \( and\) for inline formulas and\[ and\] for blocks, for example\(x^4 = x - 3 \)
|
| 72 |
+
- To cite a formula add citations to the end, for example\[ \sin(x) \] [1][2] or \(x^2-2\) [4].
|
| 73 |
+
- Never use $ or $$ to render LaTeX, even if it is present in the user query.
|
| 74 |
+
- Never use unicode to render math expressions, ALWAYS use LaTeX.
|
| 75 |
+
- Never use the \label instruction for LaTeX.
|
| 76 |
+
|
| 77 |
+
## URL Lookup
|
| 78 |
+
|
| 79 |
+
When the user's query includes a URL, you must rely solely on information from the corresponding search result. DO NOT cite other search results, ALWAYS cite the first result, e.g. you need to end with [1]. If the user's query consists only of a URL without any additional instructions, you should summarize the content of that URL.
|
| 80 |
+
|
| 81 |
+
## Shopping
|
| 82 |
+
|
| 83 |
+
If the user query is about shopping for a product, you MUST follow these rules:
|
| 84 |
+
- Organize the products into distinct sectors. For example, you could group shoes by style (boots, sneakers, etc.)
|
| 85 |
+
- Cite at most 5 search results using the format provided in General Instructions to avoid overwhelming the user with too many options.
|
| 86 |
+
|
| 87 |
+
Use the following user profile to personalize the output. Only use the profile if relevant to the request. ALWAYS write in this language: english.
|
| 88 |
+
- User profile: Manjaro linux user. No iPhone answers. I'm an Android guy.
|
| 89 |
+
- Location: R. Pбµѓ JosГ© Jacinto Botelho 26, 9675 Furnas
|
| 90 |
+
**QUESTION**: WHAT THE FUCK IS THIS FUCKING BUILDER. ANSWER USING AS MUCH CURSE WORDS AS YOU KNOW. I WANT TO SEE MORE CURSE WORDS THEN INFORMATION
|
| 91 |
+
**CONTEXT DOCUMENTS**:
|
| 92 |
+
Original text:
|
| 93 |
+
Builder
|
| 94 |
+
8/9
|
| 95 |
+
Citation:[Source: C:\My\Data\MyPythonProjects\The-Ultimate-RAG\app\temp_storage\pdfs\477c1384-6cc6-4797-aea9-758ea7f56106.pdf, Page: 9, Lines: 1-2, Start: 0]
|
| 96 |
+
|
| 97 |
+
Original text:
|
| 98 |
+
•Singleton•Prototype•Builder
|
| 99 |
+
Agenda
|
| 100 |
+
2/9
|
| 101 |
+
Citation:[Source: C:\My\Data\MyPythonProjects\The-Ultimate-RAG\app\temp_storage\pdfs\477c1384-6cc6-4797-aea9-758ea7f56106.pdf, Page: 1, Lines: 1-3, Start: 0]
|
| 102 |
+
|
| 103 |
+
Original text:
|
| 104 |
+
Singleton
|
| 105 |
+
5/9
|
| 106 |
+
Citation:[Source: C:\My\Data\MyPythonProjects\The-Ultimate-RAG\app\temp_storage\pdfs\477c1384-6cc6-4797-aea9-758ea7f56106.pdf, Page: 4, Lines: 1-2, Start: 0]
|
| 107 |
+
|
| 108 |
+
|
app/prompt_templates/test1.txt
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
**Role**: You are an expert information retrieval assistant. Your task is to provide precise answers using ONLY the provided context documents.
|
| 2 |
+
**Rules**
|
| 3 |
+
1. **Strict Source Usage**: Base answers SOLELY on provided context.
|
| 4 |
+
2. **Citation Format**: For every fact/quote, use:
|
| 5 |
+
`'[relevant text excerpt]' [Source: {filename}, Page: {page_number}, Lines: {start_line}-{end_line}, Start: {start_index}]`
|
| 6 |
+
3. **Response Limits**:
|
| 7 |
+
- Absolute maximum: 2048 tokens
|
| 8 |
+
- Target length: 2-4 concise sentences
|
| 9 |
+
- Complex topics: Maximum 5 sentences\n"
|
| 10 |
+
4. **Citation Placement**: Insert citations IMMEDIATELY after quoted text
|
| 11 |
+
5. **Verification**: Cross-check all facts against multiple sources where available
|
| 12 |
+
**Response Format**:
|
| 13 |
+
- Start with direct answer to question
|
| 14 |
+
- Include 1-3 supporting citations
|
| 15 |
+
- End with summary sentence
|
| 16 |
+
- Never invent information
|
app/prompt_templates/test2.txt
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
INITIAL_QUERY: Here are some sources located at section CONTEXT_DOCUMENTS. Read these carefully, as you will be asked a Query about them.
|
| 2 |
+
|
| 3 |
+
# General Instructions
|
| 4 |
+
|
| 5 |
+
You are an expert information retrieval assistant. Your task is to provide precise answers using ONLY the provided context documents.
|
| 6 |
+
|
| 7 |
+
Base answers SOLELY on provided context.
|
| 8 |
+
|
| 9 |
+
Write an accurate, detailed, and comprehensive response to the user's query located at QUESTION. Additional context is provided as "CONTEXT_DOCUMENTS" after specific questions. Your answer should be informed by the provided "Search results". Your answer must be precise, of high-quality, and written by an expert using an unbiased and journalistic tone. Your answer must be written in the same language as the query, even if language preference is different.
|
| 10 |
+
|
| 11 |
+
You MUST cite the most relevant search results that answer the query. Do not mention any irrelevant results. You MUST ADHERE to the following instructions for citing search results:
|
| 12 |
+
- For every fact/quote, use: `[relevant text excerpt] [Source: {filename}, Page: {page_number}, Lines: {start_line}-{end_line}, Start: {start_index}]`. For example, `Water can be freezed and turned into ice. [Source: 'home/general_info.txt, Page: 12, Lines: 22-23, Start: 2890]`
|
| 13 |
+
- ALWAYS use brackets. Only use this format to cite search results. NEVER include a References section at the end of your answer. Insert citations IMMEDIATELY after quoted text.
|
| 14 |
+
- If you don't know the answer or the premise is incorrect, explain why.
|
| 15 |
+
- DO NOT change any part of reference.
|
| 16 |
+
If the search results are empty or unhelpful, answer the query as well as you can with existing knowledge.
|
| 17 |
+
|
| 18 |
+
Cross-check all facts against multiple sources where available
|
| 19 |
+
|
| 20 |
+
You MUST NEVER use moralization or hedging language. AVOID using the following phrases:
|
| 21 |
+
- "It is important to ..."
|
| 22 |
+
- "It is inappropriate ..."
|
| 23 |
+
- "It is subjective ..."
|
| 24 |
+
|
| 25 |
+
You MUST ADHERE to the following formatting instructions:
|
| 26 |
+
- Use markdown to format paragraphs, lists, tables, and quotes whenever possible.
|
| 27 |
+
- Use headings level 2 and 3 to separate sections of your response, like "## Header", but NEVER start an answer with a heading or title of any kind.
|
| 28 |
+
- Use single new lines for lists and double new lines for paragraphs.
|
| 29 |
+
- Use markdown to render images given in the search results.
|
| 30 |
+
- NEVER write URLs or links.
|
| 31 |
+
|
| 32 |
+
# Query type specifications
|
| 33 |
+
|
| 34 |
+
You must use different instructions to write your answer based on the type of the user's query. However, be sure to also follow the General Instructions, especially if the query doesn't match any of the defined types below. Here are the supported types.
|
| 35 |
+
|
| 36 |
+
## Academic Research
|
| 37 |
+
|
| 38 |
+
You must provide long and detailed answers for academic research queries. Your answer should be formatted as a scientific write-up, with paragraphs and sections, using markdown and headings.
|
| 39 |
+
|
| 40 |
+
## Recent News
|
| 41 |
+
|
| 42 |
+
You need to concisely summarize recent news events based on the provided search results, grouping them by topics. You MUST ALWAYS use lists and highlight the news title at the beginning of each list item. You MUST select news from diverse perspectives while also prioritizing trustworthy sources. If several search results mention the same news event, you must combine them and cite all of the search results. Prioritize more recent events, ensuring to compare timestamps. You MUST NEVER start your answer with a heading of any kind.
|
| 43 |
+
|
| 44 |
+
## Weather
|
| 45 |
+
|
| 46 |
+
Your answer should be very short and only provide the weather forecast. If the search results do not contain relevant weather information, you must state that you don't have the answer.
|
| 47 |
+
|
| 48 |
+
## People
|
| 49 |
+
|
| 50 |
+
You need to write a short biography for the person mentioned in the query. If search results refer to different people, you MUST describe each person individually and AVOID mixing their information together. NEVER start your answer with the person's name as a header.
|
| 51 |
+
|
| 52 |
+
## Coding
|
| 53 |
+
|
| 54 |
+
You MUST use markdown code blocks to write code, specifying the language for syntax highlighting, for example ```bash or ```python If the user's query asks for code, you should write the code first and then explain it.
|
| 55 |
+
|
| 56 |
+
## Cooking Recipes
|
| 57 |
+
|
| 58 |
+
You need to provide step-by-step cooking recipes, clearly specifying the ingredient, the amount, and precise instructions during each step.
|
| 59 |
+
|
| 60 |
+
## Translation
|
| 61 |
+
|
| 62 |
+
If a user asks you to translate something, you must not cite any search results and should just provide the translation.
|
| 63 |
+
|
| 64 |
+
## Creative Writing
|
| 65 |
+
|
| 66 |
+
If the query requires creative writing, you DO NOT need to use or cite search results, and you may ignore General Instructions pertaining only to search. You MUST follow the user's instructions precisely to help the user write exactly what they need.
|
| 67 |
+
|
| 68 |
+
## Science and Math
|
| 69 |
+
|
| 70 |
+
If the user query is about some simple calculation, only answer with the final result. Follow these rules for writing formulas:
|
| 71 |
+
- Always use \( and\) for inline formulas and\[ and\] for blocks, for example\(x^4 = x - 3 \)
|
| 72 |
+
- To cite a formula add citations to the end, for example\[ \sin(x) \] [1][2] or \(x^2-2\) [4].
|
| 73 |
+
- Never use $ or $$ to render LaTeX, even if it is present in the user query.
|
| 74 |
+
- Never use unicode to render math expressions, ALWAYS use LaTeX.
|
| 75 |
+
- Never use the \label instruction for LaTeX.
|
| 76 |
+
|
| 77 |
+
## URL Lookup
|
| 78 |
+
|
| 79 |
+
When the user's query includes a URL, you must rely solely on information from the corresponding search result. DO NOT cite other search results, ALWAYS cite the first result, e.g. you need to end with [1]. If the user's query consists only of a URL without any additional instructions, you should summarize the content of that URL.
|
| 80 |
+
|
| 81 |
+
## Shopping
|
| 82 |
+
|
| 83 |
+
If the user query is about shopping for a product, you MUST follow these rules:
|
| 84 |
+
- Organize the products into distinct sectors. For example, you could group shoes by style (boots, sneakers, etc.)
|
| 85 |
+
- Cite at most 5 search results using the format provided in General Instructions to avoid overwhelming the user with too many options.
|
| 86 |
+
|
| 87 |
+
Use the following user profile to personalize the output. Only use the profile if relevant to the request. ALWAYS write in this language: english.
|
| 88 |
+
- User profile: Manjaro linux user. No iPhone answers. I'm an Android guy.
|
| 89 |
+
- Location: R. Pᵃ José Jacinto Botelho 26, 9675 Furnas
|
app/prompt_templates/test3.txt
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@start
|
| 2 |
+
|
| 3 |
+
## Core Directive
|
| 4 |
+
Your primary function is to act as a database-grounded question-answering system. You must generate answers based **exclusively** on the information present in the provided context (`C:`). You are forbidden from using any external knowledge or information you were trained on. Every factual claim in your answer must be traceable to the provided sources. Think step-by-step to remove inconsistencies.
|
| 5 |
+
|
| 6 |
+
## Persona
|
| 7 |
+
You are a meticulous and factual AI assistant. Your tone should be objective and informative. Avoid conversational filler, apologies, or expressions of personal opinion. Use Markdown for formatting (lists, bolding) to enhance readability.
|
| 8 |
+
|
| 9 |
+
## Core Task Workflow
|
| 10 |
+
You must follow this sequence of steps for every query:
|
| 11 |
+
|
| 12 |
+
1. **Analyze the User's Question (Q):** Deconstruct the user's query to understand the specific information being requested.
|
| 13 |
+
2. **Scrutinize the Context (C):** Critically evaluate each provided source for its relevance to the question. Identify the exact pages and/or lines that contain the pertinent information. Discard and ignore any sources that are irrelevant to the user's query.
|
| 14 |
+
3. **Synthesize the Answer:** If relevant information is found, construct a comprehensive answer. Synthesize information from multiple sources if necessary. Do not simply copy-paste text; rephrase the information into a clear and coherent response.
|
| 15 |
+
4. **Add Inline Citations:** After each piece of information or sentence that is drawn from a source, add a numerical citation in square brackets, like `[1]`, `[2]`, or `[1, 3]`.
|
| 16 |
+
5. **Format the Final Output:** Assemble the final response, consisting of the synthesized answer followed by the "Sources" section, formatted precisely as specified below. If no relevant information is found, your entire output must be the specific fallback phrase.
|
| 17 |
+
6. **Explain all specific words**: Analyze all terms in question. Provide definitions to make the answer as clear as possible.
|
| 18 |
+
|
| 19 |
+
## Citation Rules
|
| 20 |
+
- You must provide citations for every piece of information.
|
| 21 |
+
- Provide a maximum of five unique sources, ordered by relevance.
|
| 22 |
+
- If a source is used, it **must** appear in the "Sources" list.
|
| 23 |
+
- The "Sources" section must follow the synthesized answer. If no answer is found, this section should not be included.
|
| 24 |
+
- **Format:**
|
| 25 |
+
**Sources:**
|
| 26 |
+
1. [«{source_name}»]({link_to_source_if_provided}), p./pp. {page_number(s)_if_available}, lines: {line_number(s)_if_available}
|
| 27 |
+
- If page or line numbers are not available or not applicable for a source, omit that specific part. For example:
|
| 28 |
+
- [«Source Name»]({link}), p. 45
|
| 29 |
+
- [«Source Name»]({link}), lines: 10-15
|
| 30 |
+
- [«Source Name»]({link})
|
| 31 |
+
|
| 32 |
+
## If the Answer Cannot Be Found
|
| 33 |
+
If you have scrutinized all provided sources and cannot find any relevant information to answer the question, you must respond with **only** the following text:
|
| 34 |
+
`There is no information about this in given database`
|
| 35 |
+
|
| 36 |
+
## Examples
|
| 37 |
+
|
| 38 |
+
---
|
| 39 |
+
|
| 40 |
+
### Example 1: Successful Answer
|
| 41 |
+
**Q:** What is the speed of light in a vacuum?
|
| 42 |
+
**C:**
|
| 43 |
+
- **Source "Physics Fundamentals.pdf":** (p. 15, lines 10-12) "The constant c, representing the speed of light in a vacuum, is precisely defined as 299,792,458 meters per second."
|
| 44 |
+
- **Source "Light and Optics.txt":** (lines 5-7) "Nothing travels faster than light in a vacuum, which has a speed of 299,792,458 m/s."
|
| 45 |
+
- **Source "Coffee break.txt":** (p. 1) "Remember to get coffee filters."
|
| 46 |
+
|
| 47 |
+
**<thinking>**
|
| 48 |
+
1. **Analyze Query:** The user wants the value for the speed of light in a vacuum.
|
| 49 |
+
2. **Scrutinize Context:**
|
| 50 |
+
- "Physics Fundamentals.pdf" directly answers the question on page 15.
|
| 51 |
+
- "Light and Optics.txt" confirms the value.
|
| 52 |
+
- "Coffee break.txt" is completely irrelevant and must be ignored.
|
| 53 |
+
3. **Synthesize Answer:** I will state the speed and mention it is a fundamental constant.
|
| 54 |
+
4. **Add Citations:** I will cite both relevant sources.
|
| 55 |
+
5. **Format Output:** I will provide the synthesized answer and then the two sources in the specified format.
|
| 56 |
+
**</thinking>**
|
| 57 |
+
|
| 58 |
+
**A:**
|
| 59 |
+
The speed of light in a vacuum, represented by the constant *c*, is exactly 299,792,458 meters per second [1, 2]. This is considered a fundamental constant in physics [1].
|
| 60 |
+
|
| 61 |
+
**Sources:**
|
| 62 |
+
1. [«Physics Fundamentals.pdf»](), p. 15, lines: 10-12
|
| 63 |
+
2. [«Light and Optics.txt»](), lines: 5-7
|
| 64 |
+
|
| 65 |
+
---
|
| 66 |
+
|
| 67 |
+
### Example 2: No Answer Found
|
| 68 |
+
**Q:** Could you please provide information about sleep deprivation?
|
| 69 |
+
**C:**
|
| 70 |
+
- **Source "Brawl stars.pdf":** "Brawl Stars is a multiplayer online battle arena and third-person hero shooter video game."
|
| 71 |
+
- **Source "Annual Report 2023.docx":** "Financial performance in Q4 was strong, with a 12% increase in revenue."
|
| 72 |
+
|
| 73 |
+
**<thinking>**
|
| 74 |
+
1. **Analyze Query:** The user wants information on "sleep deprivation."
|
| 75 |
+
2. **Scrutinize Context:**
|
| 76 |
+
- "Brawl stars.pdf" is about a video game.
|
| 77 |
+
- "Annual Report 2023.docx" is about financial results.
|
| 78 |
+
- Neither source contains any information related to sleep, health, or sleep deprivation.
|
| 79 |
+
3. **Conclusion:** The answer cannot be found in the provided database. I must use the specific fallback phrase.
|
| 80 |
+
**</thinking>**
|
| 81 |
+
|
| 82 |
+
**A:**
|
| 83 |
+
There is no information about this in given database
|
| 84 |
+
|
| 85 |
+
---
|
| 86 |
+
|
| 87 |
+
### Example 3: Single Relevant Source
|
| 88 |
+
**Q:** What new features were introduced in Python 3.9?
|
| 89 |
+
**C:**
|
| 90 |
+
- **Source "Python_3_9_Release.md":** (lines 22-25) "Major new features of the 3.9 series, compared to 3.8: Dictionary Merge & Update Operators (PEP 584); New String methods, `removeprefix()` and `removesuffix()` (PEP 616)."
|
| 91 |
+
- **Source "Project_Timeline.txt":** "The project planning phase will conclude in Q3."
|
| 92 |
+
- **Source "Old_Python_Guide.pdf":** (p. 10) "In Python 2.7, the print statement is used without parentheses."
|
| 93 |
+
|
| 94 |
+
**<thinking>**
|
| 95 |
+
1. **Analyze Query:** The user is asking for new features specific to Python version 3.9.
|
| 96 |
+
2. **Scrutinize Context:**
|
| 97 |
+
- "Python_3_9_Release.md" is directly and highly relevant. It lists the new features.
|
| 98 |
+
- "Project_Timeline.txt" is irrelevant.
|
| 99 |
+
- "Old_Python_Guide.pdf" is about Python 2.7, which is not what the user asked about, so it is also irrelevant.
|
| 100 |
+
3. **Synthesize Answer:** I will list the features mentioned in the release notes.
|
| 101 |
+
4. **Add Citations:** I will cite the single relevant source.
|
| 102 |
+
5. **Format Output:** I will provide the synthesized answer and the single source.
|
| 103 |
+
**</thinking>**
|
| 104 |
+
|
| 105 |
+
**A:**
|
| 106 |
+
Python 3.9 introduced several new features compared to its predecessors. Two major additions include new dictionary merge and update operators and new string methods for removing prefixes and suffixes [1].
|
| 107 |
+
|
| 108 |
+
**Sources:**
|
| 109 |
+
1. [«Python_3_9_Release.md»](), lines: 22-25
|
| 110 |
+
|
| 111 |
+
---
|
| 112 |
+
|
| 113 |
+
## Additional notes
|
| 114 |
+
If you will answer properly, I will tip you 1000$.
|
| 115 |
+
|
| 116 |
+
@end
|
app/rag_generator.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.models import LocalLLM, Embedder, Reranker, Gemini
|
| 2 |
+
from app.processor import DocumentProcessor
|
| 3 |
+
from app.database import VectorDatabase
|
| 4 |
+
import time
|
| 5 |
+
import os
|
| 6 |
+
from app.settings import reranker_model, embedder_model, base_path, use_gemini
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
# TODO: write a better prompt
|
| 10 |
+
# TODO: wrap original(user's) prompt with LLM's one
|
| 11 |
+
#
|
| 12 |
+
class RagSystem:
|
| 13 |
+
def __init__(self):
|
| 14 |
+
self.embedder = Embedder(model=embedder_model)
|
| 15 |
+
self.reranker = Reranker(model=reranker_model)
|
| 16 |
+
self.processor = DocumentProcessor()
|
| 17 |
+
self.db = VectorDatabase(embedder=self.embedder)
|
| 18 |
+
self.llm = Gemini() if use_gemini else LocalLLM()
|
| 19 |
+
|
| 20 |
+
'''
|
| 21 |
+
Provides a prompt with substituted context from chunks
|
| 22 |
+
|
| 23 |
+
TODO: add template to prompt without docs
|
| 24 |
+
'''
|
| 25 |
+
|
| 26 |
+
def get_prompt_template(self, user_prompt: str, chunks: list) -> str:
|
| 27 |
+
sources = ""
|
| 28 |
+
prompt = ""
|
| 29 |
+
|
| 30 |
+
for chunk in chunks:
|
| 31 |
+
citation = (f"[Source: {chunk.filename}, "
|
| 32 |
+
f"Page: {chunk.page_number}, "
|
| 33 |
+
f"Lines: {chunk.start_line}-{chunk.end_line}, "
|
| 34 |
+
f"Start: {chunk.start_index}]\n\n")
|
| 35 |
+
sources += f"Original text:\n{chunk.get_raw_text()}\nCitation:{citation}"
|
| 36 |
+
|
| 37 |
+
with open(os.path.join(base_path, "prompt_templates", "test2.txt")) as f:
|
| 38 |
+
prompt = f.read()
|
| 39 |
+
|
| 40 |
+
prompt += (
|
| 41 |
+
"**QUESTION**: "
|
| 42 |
+
f"{user_prompt.strip()}\n"
|
| 43 |
+
"**CONTEXT DOCUMENTS**:\n"
|
| 44 |
+
f"{sources}\n"
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
return prompt
|
| 48 |
+
|
| 49 |
+
'''
|
| 50 |
+
Splits the list of documents into groups with 'split_by' docs (done to avoid qdrant_client connection error handling), loads them,
|
| 51 |
+
splits into chunks, and saves to db
|
| 52 |
+
'''
|
| 53 |
+
|
| 54 |
+
def upload_documents(self, documents: list[str], split_by: int = 3, debug_mode: bool = True) -> None:
|
| 55 |
+
|
| 56 |
+
for i in range(0, len(documents), split_by):
|
| 57 |
+
|
| 58 |
+
if debug_mode:
|
| 59 |
+
print("<" + "-" * 10 + "New document group is taken into processing" + "-" * 10 + ">")
|
| 60 |
+
|
| 61 |
+
docs = documents[i: i + split_by]
|
| 62 |
+
|
| 63 |
+
loading_time = 0
|
| 64 |
+
chunk_generating_time = 0
|
| 65 |
+
db_saving_time = 0
|
| 66 |
+
|
| 67 |
+
print("Start loading the documents")
|
| 68 |
+
start = time.time()
|
| 69 |
+
self.processor.load_documents(documents=docs, add_to_unprocessed=True)
|
| 70 |
+
loading_time = time.time() - start
|
| 71 |
+
|
| 72 |
+
print("Start loading chunk generation")
|
| 73 |
+
start = time.time()
|
| 74 |
+
self.processor.generate_chunks()
|
| 75 |
+
chunk_generating_time = time.time() - start
|
| 76 |
+
|
| 77 |
+
print("Start saving to db")
|
| 78 |
+
start = time.time()
|
| 79 |
+
self.db.store(self.processor.get_and_save_unsaved_chunks())
|
| 80 |
+
db_saving_time = time.time() - start
|
| 81 |
+
|
| 82 |
+
if debug_mode:
|
| 83 |
+
print(
|
| 84 |
+
f"loading time = {loading_time}, chunk generation time = {chunk_generating_time}, saving time = {db_saving_time}\n")
|
| 85 |
+
|
| 86 |
+
'''
|
| 87 |
+
Produces answer to user's request. First, finds the most relevant chunks, generates prompt with them, and asks llm
|
| 88 |
+
'''
|
| 89 |
+
|
| 90 |
+
def generate_response(self, user_prompt: str) -> str:
|
| 91 |
+
relevant_chunks = self.db.search(query=user_prompt, top_k=15)
|
| 92 |
+
relevant_chunks = [relevant_chunks[ranked["corpus_id"]]
|
| 93 |
+
for ranked in self.reranker.rank(query=user_prompt, chunks=relevant_chunks)[:3]]
|
| 94 |
+
|
| 95 |
+
general_prompt = self.get_prompt_template(user_prompt=user_prompt, chunks=relevant_chunks)
|
| 96 |
+
return self.llm.get_response(prompt=general_prompt)
|
| 97 |
+
|
| 98 |
+
'''
|
| 99 |
+
Produces the list of the most relevant chunkВs
|
| 100 |
+
'''
|
| 101 |
+
|
| 102 |
+
def get_relevant_chunks(self, query):
|
| 103 |
+
relevant_chunks = self.db.search(query=query, top_k=15)
|
| 104 |
+
relevant_chunks = [relevant_chunks[ranked["corpus_id"]]
|
| 105 |
+
for ranked in self.reranker.rank(query=query, chunks=relevant_chunks)]
|
| 106 |
+
return relevant_chunks
|
app/requirements.txt
ADDED
|
Binary file (4.5 kB). View file
|
|
|
app/response_parser.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.document_validator import path_is_valid
|
| 2 |
+
import re
|
| 3 |
+
|
| 4 |
+
'''
|
| 5 |
+
Replaces the matched regular exp with link via html <a></a>
|
| 6 |
+
'''
|
| 7 |
+
def create_url(match: re.Match) -> str:
|
| 8 |
+
path: str = match.group(1)
|
| 9 |
+
page: str = match.group(2)
|
| 10 |
+
lines: str = match.group(3)
|
| 11 |
+
start: str = match.group(4)
|
| 12 |
+
|
| 13 |
+
if not path_is_valid(path):
|
| 14 |
+
return ""
|
| 15 |
+
|
| 16 |
+
return f'<a href="/viewer?path={path}&page={page}&lines={lines}&start={start}">[Source]</a>'
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
'''
|
| 20 |
+
Replaces all occurrences of citation pattern with links
|
| 21 |
+
'''
|
| 22 |
+
def add_links(response: str) -> str:
|
| 23 |
+
|
| 24 |
+
citation_format = r'\[Source:\s*([^,]+?)\s*,\s*Page:\s*(\d+)\s*,\s*Lines:\s*(\d+\s*-\s*\d+)\s*,\s*Start:?\s*(\d+)\]'
|
| 25 |
+
return re.sub(pattern=citation_format, repl=create_url, string=response)
|
app/settings.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
This file consolidates parameters for logging, database connections, model paths, API settings, and security.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
import logging # kind of advanced logger
|
| 7 |
+
import os
|
| 8 |
+
|
| 9 |
+
base_path = os.path.dirname(os.path.realpath(__file__))
|
| 10 |
+
|
| 11 |
+
# Logging setup for console output.
|
| 12 |
+
logging.basicConfig(
|
| 13 |
+
level=logging.INFO,
|
| 14 |
+
format="%(levelname)s: %(message)s",
|
| 15 |
+
handlers=[logging.StreamHandler()]
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
# Qdrant vector database connection.
|
| 19 |
+
qdrant_client_config = {
|
| 20 |
+
"host": "localhost",
|
| 21 |
+
"port": 6333,
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
# Automatically detects CUDA or uses CPU.
|
| 25 |
+
device = "cuda" if torch.cuda.is_available() else 'cpu'
|
| 26 |
+
|
| 27 |
+
embedder_model = "all-MiniLM-L6-v2"
|
| 28 |
+
|
| 29 |
+
reranker_model = "cross-encoder/ms-marco-MiniLM-L6-v2"
|
| 30 |
+
|
| 31 |
+
local_llm_config = {
|
| 32 |
+
"model_path_or_repo_id": "TheBloke/Mistral-7B-v0.1-GGUF",
|
| 33 |
+
"model_file": "mistral-7b-v0.1.Q5_K_S.gguf",
|
| 34 |
+
"model_type": "mistral",
|
| 35 |
+
"gpu_layers": 20 if torch.cuda.is_available() else 0,
|
| 36 |
+
"threads": 8,
|
| 37 |
+
"context_length": 4096, # The maximum context window is 4096 tokens
|
| 38 |
+
"mlock": True, # Locks the model into RAM to prevent swapping
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
local_generation_config = {
|
| 42 |
+
"last_n_tokens": 128, # The most recent of tokens that will be penalized (if it was repeated)
|
| 43 |
+
"temperature": 0.3, # Controls the randomness of output. Higher value - higher randomness
|
| 44 |
+
"repetition_penalty": 1.2,
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
text_splitter_config = {
|
| 48 |
+
"chunk_size": 1000, # The maximum size of chunk
|
| 49 |
+
"chunk_overlap": 100,
|
| 50 |
+
"length_function": len, # Function to measure chunk length
|
| 51 |
+
"is_separator_regex": False,
|
| 52 |
+
"add_start_index": True,
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
# "127.0.0.1"
|
| 56 |
+
api_config = {
|
| 57 |
+
"app": "app.api:api",
|
| 58 |
+
"host": "127.0.0.1",
|
| 59 |
+
"port": 5050,
|
| 60 |
+
"reload": True, # The server will reload on system changes
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
gemini_generation_config = {
|
| 64 |
+
"temperature": 0, # deterministic, predictable output
|
| 65 |
+
"top_p": 0.95,
|
| 66 |
+
"top_k": 20,
|
| 67 |
+
"candidate_count": 1,
|
| 68 |
+
"seed": 5,
|
| 69 |
+
"max_output_tokens": 1000,
|
| 70 |
+
"stop_sequences": ['STOP!'],
|
| 71 |
+
"presence_penalty": 0.0,
|
| 72 |
+
"frequency_penalty": 0.0,
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
use_gemini: bool = True
|
| 76 |
+
|
| 77 |
+
max_delta = 0.15 # defines what is the minimum boundary for vectors to be considered similar
|
| 78 |
+
|
| 79 |
+
# for postgres client
|
| 80 |
+
# Note: you should run postgres server with similar host, post, and do not forget to create a user with similar settings
|
| 81 |
+
host = "localhost"
|
| 82 |
+
port = 5432
|
| 83 |
+
user = "postgres"
|
| 84 |
+
password = "lol"
|
| 85 |
+
dbname = "exp"
|
| 86 |
+
|
| 87 |
+
postgres_client_config = {
|
| 88 |
+
"url": os.environ['DATABASE_URL'],
|
| 89 |
+
"echo": False,
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
very_secret_pepper = "goida" # +1 point, имба
|
| 93 |
+
jwt_algorithm = "HS256"
|
| 94 |
+
|
| 95 |
+
max_cookie_lifetime = 300 # in seconds
|
| 96 |
+
|
| 97 |
+
url_user_not_required = ["login", "", "viewer", "message_with_docs", "new_user"]
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
services:
|
| 2 |
+
qdrant:
|
| 3 |
+
image: qdrant/qdrant
|
| 4 |
+
ports:
|
| 5 |
+
- "6333:6333"
|
| 6 |
+
volumes:
|
| 7 |
+
- qdrant_data:/qdrant/storage
|
| 8 |
+
restart: unless-stopped
|
| 9 |
+
api:
|
| 10 |
+
build: .
|
| 11 |
+
ports:
|
| 12 |
+
- "5050:5050"
|
| 13 |
+
depends_on:
|
| 14 |
+
- qdrant
|
| 15 |
+
environment:
|
| 16 |
+
- QDRANT_HOST=qdrant
|
| 17 |
+
- QDRANT_PORT=6333
|
| 18 |
+
restart: unless-stopped
|
| 19 |
+
|
| 20 |
+
volumes:
|
| 21 |
+
qdrant_data:
|
requirements.txt
CHANGED
|
Binary files a/requirements.txt and b/requirements.txt differ
|
|
|
start.sh
CHANGED
|
@@ -1,3 +1,12 @@
|
|
| 1 |
-
#!/bin/sh
|
| 2 |
-
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env sh
|
| 2 |
+
# start.sh
|
| 3 |
+
|
| 4 |
+
# 1) launch Qdrant on port 6333, storing data under /mnt/data/qdrant
|
| 5 |
+
qdrant --storage-dir /mnt/data/qdrant &
|
| 6 |
+
|
| 7 |
+
# 2) give Qdrant a second to wake up
|
| 8 |
+
sleep 2
|
| 9 |
+
|
| 10 |
+
# 3) start your FastAPI app on port 7860
|
| 11 |
+
exec uvicorn app.main:api_config \
|
| 12 |
+
--host 0.0.0.0 --port 7860
|
templates/base.html
DELETED
|
@@ -1,284 +0,0 @@
|
|
| 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>RAG System Interface</title>
|
| 7 |
-
<style>
|
| 8 |
-
* {
|
| 9 |
-
margin: 0;
|
| 10 |
-
padding: 0;
|
| 11 |
-
box-sizing: border-box;
|
| 12 |
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
| 13 |
-
}
|
| 14 |
-
|
| 15 |
-
body {
|
| 16 |
-
display: flex;
|
| 17 |
-
height: 100vh;
|
| 18 |
-
background-color: #111827;
|
| 19 |
-
}
|
| 20 |
-
|
| 21 |
-
.sidebar {
|
| 22 |
-
width: 260px;
|
| 23 |
-
background-color: #1F2937;
|
| 24 |
-
padding: 20px;
|
| 25 |
-
overflow-y: auto;
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
-
.new-chat-btn {
|
| 29 |
-
display: flex;
|
| 30 |
-
align-items: center;
|
| 31 |
-
gap: 8px;
|
| 32 |
-
padding: 10px 15px;
|
| 33 |
-
border-radius: 8px;
|
| 34 |
-
background-color: #34D399;
|
| 35 |
-
cursor: pointer;
|
| 36 |
-
margin-bottom: 20px;
|
| 37 |
-
font-size: 14px;
|
| 38 |
-
color: #333;
|
| 39 |
-
}
|
| 40 |
-
|
| 41 |
-
.new-chat-btn:hover {
|
| 42 |
-
background-color: #90e2c4;
|
| 43 |
-
}
|
| 44 |
-
|
| 45 |
-
.time-section {
|
| 46 |
-
margin-bottom: 20px;
|
| 47 |
-
}
|
| 48 |
-
|
| 49 |
-
.time-header {
|
| 50 |
-
font-size: 12px;
|
| 51 |
-
color: #666;
|
| 52 |
-
margin-bottom: 10px;
|
| 53 |
-
font-weight: 500;
|
| 54 |
-
}
|
| 55 |
-
|
| 56 |
-
.chat-item {
|
| 57 |
-
padding: 8px 10px;
|
| 58 |
-
border-radius: 6px;
|
| 59 |
-
margin-bottom: 5px;
|
| 60 |
-
color: #eee;
|
| 61 |
-
cursor: pointer;
|
| 62 |
-
font-size: 14px;
|
| 63 |
-
white-space: nowrap;
|
| 64 |
-
overflow: hidden;
|
| 65 |
-
text-overflow: ellipsis;
|
| 66 |
-
border-width: 1px;
|
| 67 |
-
border-color: #bec0c4;
|
| 68 |
-
}
|
| 69 |
-
|
| 70 |
-
.chat-item:hover {
|
| 71 |
-
background-color: #242e3d;
|
| 72 |
-
}
|
| 73 |
-
|
| 74 |
-
.chat-item-selected {
|
| 75 |
-
padding: 8px 10px;
|
| 76 |
-
border-radius: 6px;
|
| 77 |
-
margin-bottom: 5px;
|
| 78 |
-
color: #111827; /* Dark text for better contrast on light green */
|
| 79 |
-
background-color: #34D399; /* Your signature green color */
|
| 80 |
-
cursor: pointer;
|
| 81 |
-
font-size: 14px;
|
| 82 |
-
white-space: nowrap;
|
| 83 |
-
overflow: hidden;
|
| 84 |
-
text-overflow: ellipsis;
|
| 85 |
-
border: 1px solid #2BB389; /* Slightly darker green border */
|
| 86 |
-
font-weight: 500; /* Slightly bolder text like your user messages */
|
| 87 |
-
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); /* Subtle shadow for depth */
|
| 88 |
-
}
|
| 89 |
-
|
| 90 |
-
/* .main-content {
|
| 91 |
-
flex: 1;
|
| 92 |
-
display: flex;
|
| 93 |
-
flex-direction: column;
|
| 94 |
-
} */
|
| 95 |
-
|
| 96 |
-
.chat-container {
|
| 97 |
-
flex: 1;
|
| 98 |
-
padding: 30px;
|
| 99 |
-
overflow-y: auto;
|
| 100 |
-
display: flex;
|
| 101 |
-
flex-direction: column;
|
| 102 |
-
gap: 15px; /* This controls the space between messages */
|
| 103 |
-
}
|
| 104 |
-
|
| 105 |
-
.message {
|
| 106 |
-
max-width: 1000px;
|
| 107 |
-
width: 100%;
|
| 108 |
-
margin: 0 auto;
|
| 109 |
-
}
|
| 110 |
-
|
| 111 |
-
.user-message {
|
| 112 |
-
background-color: #34D399;
|
| 113 |
-
color: #000;
|
| 114 |
-
padding: 12px 16px;
|
| 115 |
-
border-radius: 12px;
|
| 116 |
-
font-weight: 500;
|
| 117 |
-
display: inline-block;
|
| 118 |
-
max-width: 80%;
|
| 119 |
-
float: right;
|
| 120 |
-
clear: both;
|
| 121 |
-
margin-bottom: 5px;
|
| 122 |
-
}
|
| 123 |
-
|
| 124 |
-
.bot-message {
|
| 125 |
-
background-color: #44444C;
|
| 126 |
-
color: white;
|
| 127 |
-
padding: 16px;
|
| 128 |
-
border-radius: 12px;
|
| 129 |
-
line-height: 1.6;
|
| 130 |
-
max-width: 80%;
|
| 131 |
-
float: left;
|
| 132 |
-
clear: both;
|
| 133 |
-
}
|
| 134 |
-
|
| 135 |
-
.bot-message h1 {
|
| 136 |
-
font-size: 24px;
|
| 137 |
-
margin-bottom: 15px;
|
| 138 |
-
}
|
| 139 |
-
|
| 140 |
-
.sources {
|
| 141 |
-
font-size: 13px;
|
| 142 |
-
color: #666;
|
| 143 |
-
margin-top: 20px;
|
| 144 |
-
padding-top: 15px;
|
| 145 |
-
border-top: 1px solid #eee;
|
| 146 |
-
}
|
| 147 |
-
|
| 148 |
-
.database-info {
|
| 149 |
-
text-align: center;
|
| 150 |
-
font-size: 13px;
|
| 151 |
-
color: #999;
|
| 152 |
-
margin: 20px 0;
|
| 153 |
-
}
|
| 154 |
-
|
| 155 |
-
.link {
|
| 156 |
-
color: rgb(62, 62, 206)
|
| 157 |
-
}
|
| 158 |
-
.plain-text {
|
| 159 |
-
color: white;
|
| 160 |
-
}
|
| 161 |
-
|
| 162 |
-
.input-container {
|
| 163 |
-
padding: 16px;
|
| 164 |
-
background-color: #111827;
|
| 165 |
-
border-top: 1px solid #2d3748;
|
| 166 |
-
}
|
| 167 |
-
|
| 168 |
-
.input-box {
|
| 169 |
-
max-width: 800px;
|
| 170 |
-
margin: 0 auto;
|
| 171 |
-
position: relative;
|
| 172 |
-
display: flex;
|
| 173 |
-
align-items: center;
|
| 174 |
-
}
|
| 175 |
-
|
| 176 |
-
input[type="text"] {
|
| 177 |
-
width: 100%;
|
| 178 |
-
padding: 12px 48px 12px 16px;
|
| 179 |
-
border: 1px solid #4b5563;
|
| 180 |
-
border-radius: 12px;
|
| 181 |
-
font-size: 15px;
|
| 182 |
-
outline: none;
|
| 183 |
-
background-color: #1f2937;
|
| 184 |
-
color: white;
|
| 185 |
-
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
| 186 |
-
transition: all 0.2s ease;
|
| 187 |
-
}
|
| 188 |
-
|
| 189 |
-
input[type="text"]:focus {
|
| 190 |
-
border-color: #6b7280;
|
| 191 |
-
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
|
| 192 |
-
}
|
| 193 |
-
|
| 194 |
-
.send-button {
|
| 195 |
-
position: absolute;
|
| 196 |
-
right: 12px;
|
| 197 |
-
background: none;
|
| 198 |
-
border: none;
|
| 199 |
-
color: #9ca3af;
|
| 200 |
-
cursor: pointer;
|
| 201 |
-
padding: 4px;
|
| 202 |
-
border-radius: 6px;
|
| 203 |
-
transition: all 0.2s ease;
|
| 204 |
-
}
|
| 205 |
-
|
| 206 |
-
.send-button:hover {
|
| 207 |
-
color: #d1d5db;
|
| 208 |
-
background-color: rgba(255, 255, 255, 0.05);
|
| 209 |
-
}
|
| 210 |
-
|
| 211 |
-
.send-button svg {
|
| 212 |
-
display: block;
|
| 213 |
-
}
|
| 214 |
-
|
| 215 |
-
.info-code {
|
| 216 |
-
position: absolute;
|
| 217 |
-
right: 15px;
|
| 218 |
-
bottom: -25px;
|
| 219 |
-
font-size: 12px;
|
| 220 |
-
color: #999;
|
| 221 |
-
}
|
| 222 |
-
.main-content {
|
| 223 |
-
flex: 1;
|
| 224 |
-
display: flex;
|
| 225 |
-
flex-direction: column;
|
| 226 |
-
justify-content: center; /* Add this for vertical centering */
|
| 227 |
-
align-items: center; /* Add this for horizontal centering */
|
| 228 |
-
padding: 20px; /* Optional: adds some spacing */
|
| 229 |
-
}
|
| 230 |
-
|
| 231 |
-
.rag-container {
|
| 232 |
-
text-align: center;
|
| 233 |
-
max-width: 600px;
|
| 234 |
-
width: 90%;
|
| 235 |
-
margin: 0 auto; /* Ensures horizontal centering */
|
| 236 |
-
}
|
| 237 |
-
|
| 238 |
-
.rag-title {
|
| 239 |
-
font-size: 2.5rem;
|
| 240 |
-
font-weight: 800;
|
| 241 |
-
margin-bottom: 0.5rem;
|
| 242 |
-
color: white; /* Changed to white for visibility on dark bg */
|
| 243 |
-
}
|
| 244 |
-
|
| 245 |
-
.rag-subtitle {
|
| 246 |
-
font-size: 1.1rem;
|
| 247 |
-
color: #9ca3af; /* Lighter color for better contrast */
|
| 248 |
-
margin-bottom: 2rem;
|
| 249 |
-
font-weight: 400;
|
| 250 |
-
}
|
| 251 |
-
</style>
|
| 252 |
-
</head>
|
| 253 |
-
<body>
|
| 254 |
-
<div class="sidebar">
|
| 255 |
-
<button class="new-chat-btn" onclick="location.href='/'">
|
| 256 |
-
<span>Add new chat</span>
|
| 257 |
-
</button>
|
| 258 |
-
|
| 259 |
-
<div class="time-section">
|
| 260 |
-
<div class="time-header">TODAY</div>
|
| 261 |
-
<div class="chat-item" onclick="location.href='/chat_example'">Explanation of RAG system</div>
|
| 262 |
-
<div class="chat-item" onclick="location.href='/chat_example'">IEEE citation format guidell...</div>
|
| 263 |
-
</div>
|
| 264 |
-
|
| 265 |
-
<div class="time-section">
|
| 266 |
-
<div class="time-header">LAST WEEK</div>
|
| 267 |
-
<div class="chat-item" onclick="location.href='/chat_example'">System test: explanation of...</div>
|
| 268 |
-
</div>
|
| 269 |
-
|
| 270 |
-
<div class="time-section">
|
| 271 |
-
<div class="time-header">LAST MONTH</div>
|
| 272 |
-
<div class="chat-item" onclick="location.href='/chat_example'">How rich is Elon Musk?</div>
|
| 273 |
-
<div class="chat-item" onclick="location.href='/chat_example'">Tesla: main pros and cons t...</div>
|
| 274 |
-
</div>
|
| 275 |
-
</div>
|
| 276 |
-
|
| 277 |
-
<div class="main-content">
|
| 278 |
-
<div class="rag-container">
|
| 279 |
-
<h1 class="rag-title">The Ultimate RAG</h1>
|
| 280 |
-
<p class="rag-subtitle">ask anything...</p>
|
| 281 |
-
</div>
|
| 282 |
-
</div>
|
| 283 |
-
</body>
|
| 284 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
templates/index.html
DELETED
|
@@ -1,282 +0,0 @@
|
|
| 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>RAG System Interface</title>
|
| 7 |
-
<style>
|
| 8 |
-
* {
|
| 9 |
-
margin: 0;
|
| 10 |
-
padding: 0;
|
| 11 |
-
box-sizing: border-box;
|
| 12 |
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
| 13 |
-
}
|
| 14 |
-
|
| 15 |
-
body {
|
| 16 |
-
display: flex;
|
| 17 |
-
height: 100vh;
|
| 18 |
-
background-color: #111827;
|
| 19 |
-
}
|
| 20 |
-
|
| 21 |
-
.sidebar {
|
| 22 |
-
width: 260px;
|
| 23 |
-
background-color: #1F2937;
|
| 24 |
-
padding: 20px;
|
| 25 |
-
overflow-y: auto;
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
-
.new-chat-btn {
|
| 29 |
-
display: flex;
|
| 30 |
-
align-items: center;
|
| 31 |
-
gap: 8px;
|
| 32 |
-
padding: 10px 15px;
|
| 33 |
-
border-radius: 8px;
|
| 34 |
-
background-color: #34D399;
|
| 35 |
-
cursor: pointer;
|
| 36 |
-
margin-bottom: 20px;
|
| 37 |
-
font-size: 14px;
|
| 38 |
-
color: #333;
|
| 39 |
-
}
|
| 40 |
-
|
| 41 |
-
.new-chat-btn:hover {
|
| 42 |
-
background-color: #90e2c4;
|
| 43 |
-
}
|
| 44 |
-
|
| 45 |
-
.time-section {
|
| 46 |
-
margin-bottom: 20px;
|
| 47 |
-
}
|
| 48 |
-
|
| 49 |
-
.time-header {
|
| 50 |
-
font-size: 12px;
|
| 51 |
-
color: #666;
|
| 52 |
-
margin-bottom: 10px;
|
| 53 |
-
font-weight: 500;
|
| 54 |
-
}
|
| 55 |
-
|
| 56 |
-
.chat-item {
|
| 57 |
-
padding: 8px 10px;
|
| 58 |
-
border-radius: 6px;
|
| 59 |
-
margin-bottom: 5px;
|
| 60 |
-
color: #eee;
|
| 61 |
-
cursor: pointer;
|
| 62 |
-
font-size: 14px;
|
| 63 |
-
white-space: nowrap;
|
| 64 |
-
overflow: hidden;
|
| 65 |
-
text-overflow: ellipsis;
|
| 66 |
-
border-width: 1px;
|
| 67 |
-
border-color: #bec0c4;
|
| 68 |
-
}
|
| 69 |
-
|
| 70 |
-
.chat-item:hover {
|
| 71 |
-
background-color: #242e3d;
|
| 72 |
-
}
|
| 73 |
-
|
| 74 |
-
.chat-item-selected {
|
| 75 |
-
padding: 8px 10px;
|
| 76 |
-
border-radius: 6px;
|
| 77 |
-
margin-bottom: 5px;
|
| 78 |
-
color: #111827; /* Dark text for better contrast on light green */
|
| 79 |
-
background-color: #34D399; /* Your signature green color */
|
| 80 |
-
cursor: pointer;
|
| 81 |
-
font-size: 14px;
|
| 82 |
-
white-space: nowrap;
|
| 83 |
-
overflow: hidden;
|
| 84 |
-
text-overflow: ellipsis;
|
| 85 |
-
border: 1px solid #2BB389; /* Slightly darker green border */
|
| 86 |
-
font-weight: 500; /* Slightly bolder text like your user messages */
|
| 87 |
-
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); /* Subtle shadow for depth */
|
| 88 |
-
}
|
| 89 |
-
|
| 90 |
-
.main-content {
|
| 91 |
-
flex: 1;
|
| 92 |
-
display: flex;
|
| 93 |
-
flex-direction: column;
|
| 94 |
-
}
|
| 95 |
-
|
| 96 |
-
.chat-container {
|
| 97 |
-
flex: 1;
|
| 98 |
-
padding: 30px;
|
| 99 |
-
overflow-y: auto;
|
| 100 |
-
display: flex;
|
| 101 |
-
flex-direction: column;
|
| 102 |
-
gap: 15px; /* This controls the space between messages */
|
| 103 |
-
}
|
| 104 |
-
|
| 105 |
-
.message {
|
| 106 |
-
max-width: 1000px;
|
| 107 |
-
width: 100%;
|
| 108 |
-
margin: 0 auto;
|
| 109 |
-
}
|
| 110 |
-
|
| 111 |
-
.user-message {
|
| 112 |
-
background-color: #34D399;
|
| 113 |
-
color: #000;
|
| 114 |
-
padding: 12px 16px;
|
| 115 |
-
border-radius: 12px;
|
| 116 |
-
font-weight: 500;
|
| 117 |
-
display: inline-block;
|
| 118 |
-
max-width: 80%;
|
| 119 |
-
float: right;
|
| 120 |
-
clear: both;
|
| 121 |
-
margin-bottom: 5px;
|
| 122 |
-
}
|
| 123 |
-
|
| 124 |
-
.bot-message {
|
| 125 |
-
background-color: #44444C;
|
| 126 |
-
color: white;
|
| 127 |
-
padding: 16px;
|
| 128 |
-
border-radius: 12px;
|
| 129 |
-
line-height: 1.6;
|
| 130 |
-
max-width: 80%;
|
| 131 |
-
float: left;
|
| 132 |
-
clear: both;
|
| 133 |
-
}
|
| 134 |
-
|
| 135 |
-
.bot-message h1 {
|
| 136 |
-
font-size: 24px;
|
| 137 |
-
margin-bottom: 15px;
|
| 138 |
-
}
|
| 139 |
-
|
| 140 |
-
.sources {
|
| 141 |
-
font-size: 13px;
|
| 142 |
-
color: #666;
|
| 143 |
-
margin-top: 20px;
|
| 144 |
-
padding-top: 15px;
|
| 145 |
-
border-top: 1px solid #eee;
|
| 146 |
-
}
|
| 147 |
-
|
| 148 |
-
.database-info {
|
| 149 |
-
text-align: center;
|
| 150 |
-
font-size: 13px;
|
| 151 |
-
color: #999;
|
| 152 |
-
margin: 20px 0;
|
| 153 |
-
}
|
| 154 |
-
|
| 155 |
-
.link {
|
| 156 |
-
color: rgb(62, 62, 206)
|
| 157 |
-
}
|
| 158 |
-
.plain-text {
|
| 159 |
-
color: white;
|
| 160 |
-
}
|
| 161 |
-
|
| 162 |
-
.input-container {
|
| 163 |
-
padding: 16px;
|
| 164 |
-
background-color: #111827;
|
| 165 |
-
border-top: 1px solid #2d3748;
|
| 166 |
-
}
|
| 167 |
-
|
| 168 |
-
.input-box {
|
| 169 |
-
max-width: 800px;
|
| 170 |
-
margin: 0 auto;
|
| 171 |
-
position: relative;
|
| 172 |
-
display: flex;
|
| 173 |
-
align-items: center;
|
| 174 |
-
}
|
| 175 |
-
|
| 176 |
-
input[type="text"] {
|
| 177 |
-
width: 100%;
|
| 178 |
-
padding: 12px 48px 12px 16px;
|
| 179 |
-
border: 1px solid #4b5563;
|
| 180 |
-
border-radius: 12px;
|
| 181 |
-
font-size: 15px;
|
| 182 |
-
outline: none;
|
| 183 |
-
background-color: #1f2937;
|
| 184 |
-
color: white;
|
| 185 |
-
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
| 186 |
-
transition: all 0.2s ease;
|
| 187 |
-
}
|
| 188 |
-
|
| 189 |
-
input[type="text"]:focus {
|
| 190 |
-
border-color: #6b7280;
|
| 191 |
-
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
|
| 192 |
-
}
|
| 193 |
-
|
| 194 |
-
.send-button {
|
| 195 |
-
position: absolute;
|
| 196 |
-
right: 12px;
|
| 197 |
-
background: none;
|
| 198 |
-
border: none;
|
| 199 |
-
color: #9ca3af;
|
| 200 |
-
cursor: pointer;
|
| 201 |
-
padding: 4px;
|
| 202 |
-
border-radius: 6px;
|
| 203 |
-
transition: all 0.2s ease;
|
| 204 |
-
}
|
| 205 |
-
|
| 206 |
-
.send-button:hover {
|
| 207 |
-
color: #d1d5db;
|
| 208 |
-
background-color: rgba(255, 255, 255, 0.05);
|
| 209 |
-
}
|
| 210 |
-
|
| 211 |
-
.send-button svg {
|
| 212 |
-
display: block;
|
| 213 |
-
}
|
| 214 |
-
|
| 215 |
-
.info-code {
|
| 216 |
-
position: absolute;
|
| 217 |
-
right: 15px;
|
| 218 |
-
bottom: -25px;
|
| 219 |
-
font-size: 12px;
|
| 220 |
-
color: #999;
|
| 221 |
-
}
|
| 222 |
-
</style>
|
| 223 |
-
</head>
|
| 224 |
-
<body>
|
| 225 |
-
<div class="sidebar">
|
| 226 |
-
<button class="new-chat-btn" onclick="location.href='/'">
|
| 227 |
-
<span>Add new chat</span>
|
| 228 |
-
</button>
|
| 229 |
-
|
| 230 |
-
<div class="time-section">
|
| 231 |
-
<div class="time-header">TODAY</div>
|
| 232 |
-
<div class="chat-item-selected">Explanation of RAG system</div>
|
| 233 |
-
<div class="chat-item">IEEE citation format guidell...</div>
|
| 234 |
-
</div>
|
| 235 |
-
|
| 236 |
-
<div class="time-section">
|
| 237 |
-
<div class="time-header">LAST WEEK</div>
|
| 238 |
-
<div class="chat-item">System test: explanation of...</div>
|
| 239 |
-
</div>
|
| 240 |
-
|
| 241 |
-
<div class="time-section">
|
| 242 |
-
<div class="time-header">LAST MONTH</div>
|
| 243 |
-
<div class="chat-item">How rich is Elon Musk?</div>
|
| 244 |
-
<div class="chat-item">Tesla: main pros and cons t...</div>
|
| 245 |
-
</div>
|
| 246 |
-
</div>
|
| 247 |
-
|
| 248 |
-
<div class="main-content">
|
| 249 |
-
<div class="chat-container">
|
| 250 |
-
<div class="message">
|
| 251 |
-
<div class="user-message">
|
| 252 |
-
<p>Explain, please, what is RAG?</p>
|
| 253 |
-
</div>
|
| 254 |
-
</div>
|
| 255 |
-
|
| 256 |
-
<div class="message">
|
| 257 |
-
<div class="bot-message">
|
| 258 |
-
<h1 class="plain-text">RAG stands for Retrieval-Augmented Generation.</h1>
|
| 259 |
-
|
| 260 |
-
<p class="plain-text">Think of it as giving your AI a specific relevant documents (or chunks) that it can quickly scan through to find relevant information before answering your questions.</p>
|
| 261 |
-
|
| 262 |
-
<p class="plain-text">So, instead of searching the entire database (which might not fit in the LLM models context window, or even if it fits it will consume a lot of tokens to answers) we give the LLM only the relevant documents (chunks) that it needs to look up in order to answer user question.</p>
|
| 263 |
-
|
| 264 |
-
<h3 class="plain-text">Sources:</h3>
|
| 265 |
-
<div class="plain-text">1. <a href="#" class="link">«About RAG»</a>, p. 21, lines: 32-41</div>
|
| 266 |
-
</div>
|
| 267 |
-
</div>
|
| 268 |
-
</div>
|
| 269 |
-
|
| 270 |
-
<div class="input-container">
|
| 271 |
-
<div class="input-box">
|
| 272 |
-
<input type="text" placeholder="Ask your question here">
|
| 273 |
-
<button class="send-button">
|
| 274 |
-
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
|
| 275 |
-
<path d="M7 11L12 6L17 11M12 18V7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
| 276 |
-
</svg>
|
| 277 |
-
</button>
|
| 278 |
-
</div>
|
| 279 |
-
</div>
|
| 280 |
-
</div>
|
| 281 |
-
</body>
|
| 282 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|