Andrchest commited on
Commit
e53c2d7
·
1 Parent(s): 451a881

final try 1

Browse files
Files changed (48) hide show
  1. .gitattributes +0 -35
  2. .gitignore +6 -0
  3. Dockerfile +40 -12
  4. LICENSE +21 -0
  5. README.md +74 -12
  6. app.py +0 -114
  7. app/__init__.py +0 -0
  8. app/api.py +295 -0
  9. app/backend/__init__.py +0 -0
  10. app/backend/controllers/__init__.py +0 -0
  11. app/backend/controllers/base_controller.py +4 -0
  12. app/backend/controllers/chats.py +5 -0
  13. app/backend/controllers/schemas.py +32 -0
  14. app/backend/controllers/users.py +145 -0
  15. app/backend/models/__init__.py +0 -0
  16. app/backend/models/base_model.py +9 -0
  17. app/backend/models/chats.py +25 -0
  18. app/backend/models/db_service.py +27 -0
  19. app/backend/models/messages.py +16 -0
  20. app/backend/models/users.py +47 -0
  21. app/chunks.py +47 -0
  22. app/database.py +146 -0
  23. app/document_validator.py +7 -0
  24. app/frontend/static/styles.css +206 -0
  25. app/frontend/templates/base.html +32 -0
  26. app/frontend/templates/components/navbar.html +8 -0
  27. app/frontend/templates/pages/chat.html +173 -0
  28. app/frontend/templates/pages/login.html +79 -0
  29. app/frontend/templates/pages/main.html +9 -0
  30. app/frontend/templates/pages/registration.html +83 -0
  31. app/frontend/templates/pages/show_pdf.html +98 -0
  32. app/frontend/templates/pages/show_text.html +47 -0
  33. app/main.py +41 -0
  34. app/models.py +105 -0
  35. app/processor.py +230 -0
  36. app/prompt.txt +108 -0
  37. app/prompt_templates/test1.txt +16 -0
  38. app/prompt_templates/test2.txt +89 -0
  39. app/prompt_templates/test3.txt +116 -0
  40. app/rag_generator.py +106 -0
  41. app/requirements.txt +0 -0
  42. app/response_parser.py +25 -0
  43. app/settings.py +97 -0
  44. docker-compose.yml +21 -0
  45. requirements.txt +0 -0
  46. start.sh +12 -3
  47. templates/base.html +0 -284
  48. 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
- FROM python:3.9
2
- RUN useradd -m -u 1000 user
3
- USER user
4
- ENV PATH="/home/user/.local/bin:$PATH"
 
 
5
  WORKDIR /app
6
- COPY --chown=user ./requirements.txt requirements.txt
7
- RUN pip install --no-cache-dir --upgrade -r requirements.txt
 
 
 
 
 
 
 
 
 
 
8
  RUN wget https://github.com/qdrant/qdrant/releases/download/v1.11.5/qdrant-x86_64-unknown-linux-gnu.tar.gz \
9
- && tar -xzf qdrant-x86_64-unknown-linux-gnu.tar.gz \
10
- && mv qdrant /home/user/.local/bin/qdrant \
11
- && rm qdrant-x86_64-unknown-linux-gnu.tar.gz
12
- COPY --chown=user . /app
13
- RUN chmod +x start.sh
14
- CMD ["./start.sh"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- title: Test
3
- emoji: 🏢
4
- colorFrom: gray
5
- colorTo: yellow
6
- sdk: docker
7
- pinned: false
8
- license: mit
9
- short_description: test
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- qdrant &
3
- uvicorn app:api --host 0.0.0.0 --port 7860
 
 
 
 
 
 
 
 
 
 
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>