readme
This commit is contained in:
parent
f67de6dcaa
commit
9dc45a687b
332
README.md
332
README.md
@ -1,34 +1,93 @@
|
|||||||
# ZP Agent
|
# ZP Agent
|
||||||
|
|
||||||
Backend pre indexovanie a vyhľadávanie v repozitári záverečných prác `zpwiki`.
|
Backend pre indexovanie, hybridné vyhľadávanie a RAG nad repozitárom
|
||||||
|
záverečných prác `zpwiki`.
|
||||||
|
|
||||||
Projekt načítava Markdown dokumenty, spracuje YAML metadata, rozdelí obsah na tokenové chunky a vytvorí SQLite index kombinujúci FTS5 fulltextové vyhľadávanie a embeddingy. Vyhľadávanie je dostupné cez FastAPI a systém podporuje manuálnu aj webhookovú synchronizáciu.
|
Projekt načítava Markdown dokumenty, spracuje YAML metadata, rozdelí
|
||||||
|
obsah na tokenové chunky a vytvorí SQLite index kombinujúci FTS5
|
||||||
|
fulltextové vyhľadávanie a embeddingy. Nad hybridným vyhľadávaním je
|
||||||
|
vytvorená RAG vrstva, ktorá pripravuje kontext, zdroje a pravidlá pre
|
||||||
|
odpoveď jazykového modelu. API je integrované so školským OpenWebUI ako
|
||||||
|
OpenAPI Tool Server.
|
||||||
|
|
||||||
## Implementované
|
## Implementované
|
||||||
|
|
||||||
- načítanie Markdown súborov a YAML front matter,
|
- načítanie Markdown súborov a YAML front matter,
|
||||||
- normalizácia názvov, autorov, tagov, kategórií a `published`,
|
- normalizácia názvov, autorov, tagov, kategórií a `published`,
|
||||||
- tokenové chunkovanie pomocou `tiktoken`,
|
- tokenové chunkovanie pomocou `tiktoken`,
|
||||||
|
- konfigurovateľná maximálna veľkosť chunku, overlap a minimálna
|
||||||
|
veľkosť chunku,
|
||||||
- zachovanie názvu dokumentu a hierarchie nadpisov v chunku,
|
- zachovanie názvu dokumentu a hierarchie nadpisov v chunku,
|
||||||
- SQLite databáza a FTS5 fulltextový index,
|
- SQLite databáza a FTS5 fulltextový index,
|
||||||
- BM25 vyhľadávanie s podporou diakritiky a prefixových výrazov,
|
- BM25 vyhľadávanie s podporou diakritiky a prefixových výrazov,
|
||||||
- embeddingy pre každý chunk pomocou modelu `intfloat/multilingual-e5-small`,
|
- vyhľadávacie stratégie `all_terms`, `prefix_terms` a `any_term`,
|
||||||
|
- embeddingy pre každý chunk pomocou modelu
|
||||||
|
`intfloat/multilingual-e5-small`,
|
||||||
- uloženie embeddingov priamo v SQLite,
|
- uloženie embeddingov priamo v SQLite,
|
||||||
- vektorové vyhľadávanie pomocou cosine similarity,
|
- vektorové vyhľadávanie pomocou cosine similarity,
|
||||||
- hybridné vyhľadávanie FTS5 + embeddings pomocou RRF,
|
- hybridné vyhľadávanie FTS5 + embeddings pomocou RRF,
|
||||||
- nižšia váha pre slabú `any_term` FTS stratégiu,
|
- nižšia váha pre slabú `any_term` FTS stratégiu,
|
||||||
- zachovanie presných `all_terms` a `prefix_terms` výsledkov bez vektorového šumu,
|
- zachovanie presných `all_terms` a `prefix_terms` výsledkov bez
|
||||||
|
vektorového šumu,
|
||||||
|
- obmedzenie počtu výsledkov z jedného dokumentu pomocou
|
||||||
|
`max_per_document`,
|
||||||
- filtrovanie publikovaných dokumentov,
|
- filtrovanie publikovaných dokumentov,
|
||||||
- FastAPI endpointy `/health`, `/search`, `/sync` a `/webhook/gitea`,
|
- generovanie `source_url` pre dohľadateľnosť výsledkov,
|
||||||
- autorizácia `/search` a `/sync` pomocou API kľúča,
|
- RAG vrstva s pripraveným kontextom, zdrojmi a pravidlami pre
|
||||||
- Gitea webhook s HMAC-SHA256 podpisom a kontrolou udalosti a repozitára,
|
grounded odpoveď,
|
||||||
|
- pravidlá proti používaniu neoverených informácií a zamieňaniu
|
||||||
|
rôznych typov údajov,
|
||||||
|
- FastAPI endpointy `/health`, `/rag`, `/search`, `/sync` a
|
||||||
|
`/webhook/gitea`,
|
||||||
|
- autorizácia vyhľadávania pomocou `X-API-Key` alebo Bearer tokenu,
|
||||||
|
- autorizácia `/sync` pomocou samostatného API kľúča,
|
||||||
|
- CORS konfigurácia pre OpenWebUI,
|
||||||
|
- integrácia s OpenWebUI cez OpenAPI Tool Server,
|
||||||
|
- Gitea webhook s HMAC-SHA256 podpisom a kontrolou udalosti a
|
||||||
|
repozitára,
|
||||||
- zámok proti súbežnému reindexovaniu,
|
- zámok proti súbežnému reindexovaniu,
|
||||||
- atomická výmena databázy po úspešnom reindexovaní,
|
- atomická výmena databázy po úspešnom reindexovaní,
|
||||||
- automatizované a integračné testy.
|
- persistentná Hugging Face cache v Docker volume,
|
||||||
|
- warm-up embeddingového modelu,
|
||||||
|
- automatizované a integračné testy vrátane RAG endpointu.
|
||||||
|
|
||||||
|
## Architektúra
|
||||||
|
|
||||||
|
``` text
|
||||||
|
zpwiki
|
||||||
|
↓
|
||||||
|
Markdown + YAML
|
||||||
|
↓
|
||||||
|
normalizácia dokumentov
|
||||||
|
↓
|
||||||
|
tokenové chunkovanie
|
||||||
|
↓
|
||||||
|
SQLite
|
||||||
|
├── dokumenty a metadata
|
||||||
|
├── chunky
|
||||||
|
├── FTS5
|
||||||
|
└── embeddingy
|
||||||
|
↓
|
||||||
|
hybridné vyhľadávanie
|
||||||
|
├── FTS5 / BM25
|
||||||
|
└── embeddingové vyhľadávanie
|
||||||
|
↓
|
||||||
|
RRF fusion
|
||||||
|
↓
|
||||||
|
RAG kontext + zdroje
|
||||||
|
↓
|
||||||
|
FastAPI /rag
|
||||||
|
↓
|
||||||
|
OpenWebUI / ZP Agent
|
||||||
|
↓
|
||||||
|
jazykový model
|
||||||
|
↓
|
||||||
|
odpoveď so source_url
|
||||||
|
```
|
||||||
|
|
||||||
## Štruktúra
|
## Štruktúra
|
||||||
|
|
||||||
```text
|
``` text
|
||||||
zp-agent/
|
zp-agent/
|
||||||
├── app/
|
├── app/
|
||||||
│ └── main.py
|
│ └── main.py
|
||||||
@ -38,6 +97,7 @@ zp-agent/
|
|||||||
│ ├── build_chunks.py
|
│ ├── build_chunks.py
|
||||||
│ ├── build_sqlite_index.py
|
│ ├── build_sqlite_index.py
|
||||||
│ ├── embedding_utils.py
|
│ ├── embedding_utils.py
|
||||||
|
│ ├── rag_utils.py
|
||||||
│ ├── rebuild_index.py
|
│ ├── rebuild_index.py
|
||||||
│ ├── search_db.py
|
│ ├── search_db.py
|
||||||
│ └── search_utils.py
|
│ └── search_utils.py
|
||||||
@ -52,7 +112,7 @@ zp-agent/
|
|||||||
|
|
||||||
Projekt očakáva repozitáre v tejto štruktúre:
|
Projekt očakáva repozitáre v tejto štruktúre:
|
||||||
|
|
||||||
```text
|
``` text
|
||||||
~/DP/
|
~/DP/
|
||||||
├── zpwiki/
|
├── zpwiki/
|
||||||
└── zp-agent/
|
└── zp-agent/
|
||||||
@ -62,7 +122,7 @@ Projekt očakáva repozitáre v tejto štruktúre:
|
|||||||
|
|
||||||
V koreňovom priečinku vytvor `.env`:
|
V koreňovom priečinku vytvor `.env`:
|
||||||
|
|
||||||
```dotenv
|
``` dotenv
|
||||||
WEBHOOK_SECRET=<náhodná hodnota s minimálne 32 znakmi>
|
WEBHOOK_SECRET=<náhodná hodnota s minimálne 32 znakmi>
|
||||||
SYNC_API_KEY=<iná náhodná hodnota s minimálne 32 znakmi>
|
SYNC_API_KEY=<iná náhodná hodnota s minimálne 32 znakmi>
|
||||||
SEARCH_API_KEY=<ďalšia náhodná hodnota s minimálne 32 znakmi>
|
SEARCH_API_KEY=<ďalšia náhodná hodnota s minimálne 32 znakmi>
|
||||||
@ -76,48 +136,66 @@ EMBEDDING_BATCH_SIZE=32
|
|||||||
|
|
||||||
Tajomstvá je možné vygenerovať príkazom:
|
Tajomstvá je možné vygenerovať príkazom:
|
||||||
|
|
||||||
```bash
|
``` bash
|
||||||
openssl rand -hex 32
|
openssl rand -hex 32
|
||||||
```
|
```
|
||||||
|
|
||||||
Súbor `.env` sa nesmie commitovať.
|
Súbor `.env` sa nesmie commitovať.
|
||||||
|
|
||||||
|
Chunkovanie je možné konfigurovať pomocou premenných prostredia:
|
||||||
|
|
||||||
|
``` dotenv
|
||||||
|
CHUNK_MAX_TOKENS=450
|
||||||
|
CHUNK_OVERLAP_TOKENS=70
|
||||||
|
CHUNK_MIN_TOKENS=80
|
||||||
|
CHUNK_TOKEN_ENCODING=cl100k_base
|
||||||
|
```
|
||||||
|
|
||||||
## Spustenie cez Docker
|
## Spustenie cez Docker
|
||||||
|
|
||||||
```bash
|
``` bash
|
||||||
docker compose build
|
docker compose up -d --build
|
||||||
docker compose up -d
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Kontrola služby:
|
Kontrola služby:
|
||||||
|
|
||||||
```bash
|
``` bash
|
||||||
curl http://127.0.0.1:8000/health
|
curl http://127.0.0.1:8000/health
|
||||||
```
|
```
|
||||||
|
|
||||||
Swagger UI:
|
Swagger UI:
|
||||||
|
|
||||||
```text
|
``` text
|
||||||
http://127.0.0.1:8000/docs
|
http://127.0.0.1:8000/docs
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Logy:
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
docker compose logs -f zp-agent-api
|
||||||
|
```
|
||||||
|
|
||||||
Zastavenie:
|
Zastavenie:
|
||||||
|
|
||||||
```bash
|
``` bash
|
||||||
docker compose down
|
docker compose down
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Hugging Face cache je uložená v persistentnom Docker volume, takže sa
|
||||||
|
embeddingový model pri bežnom reštarte kontajnera nemusí znova sťahovať.
|
||||||
|
|
||||||
## Reindexovanie
|
## Reindexovanie
|
||||||
|
|
||||||
Celý proces načíta dokumenty, vytvorí chunky, obnoví FTS5 index a vytvorí embedding pre každý chunk:
|
Celý proces načíta dokumenty, vytvorí chunky, obnoví FTS5 index a
|
||||||
|
vytvorí embedding pre každý chunk:
|
||||||
|
|
||||||
```bash
|
``` bash
|
||||||
docker compose run --rm zp-agent-api python scripts/rebuild_index.py
|
docker compose run --rm zp-agent-api python scripts/rebuild_index.py
|
||||||
```
|
```
|
||||||
|
|
||||||
Vzniknú súbory:
|
Vzniknú súbory:
|
||||||
|
|
||||||
```text
|
``` text
|
||||||
data/documents.json
|
data/documents.json
|
||||||
data/chunks.json
|
data/chunks.json
|
||||||
data/zp_index.sqlite
|
data/zp_index.sqlite
|
||||||
@ -125,11 +203,14 @@ data/zp_index.sqlite
|
|||||||
|
|
||||||
Databáza obsahuje dokumenty, chunky, FTS5 index, metadata a embeddingy.
|
Databáza obsahuje dokumenty, chunky, FTS5 index, metadata a embeddingy.
|
||||||
|
|
||||||
## Vyhľadávanie
|
Pri automatickom reindexovaní sa používa zámok proti súbežnému spusteniu
|
||||||
|
a atomická výmena databázy po úspešnom vytvorení nového indexu.
|
||||||
|
|
||||||
|
## Hybridné vyhľadávanie
|
||||||
|
|
||||||
Vyhľadávanie kombinuje:
|
Vyhľadávanie kombinuje:
|
||||||
|
|
||||||
```text
|
``` text
|
||||||
dotaz
|
dotaz
|
||||||
├── FTS5 / BM25
|
├── FTS5 / BM25
|
||||||
└── embeddingové vyhľadávanie
|
└── embeddingové vyhľadávanie
|
||||||
@ -139,16 +220,27 @@ dotaz
|
|||||||
výsledky
|
výsledky
|
||||||
```
|
```
|
||||||
|
|
||||||
|
FTS5 používa stratégie:
|
||||||
|
|
||||||
|
``` text
|
||||||
|
all_terms
|
||||||
|
prefix_terms
|
||||||
|
any_term
|
||||||
|
```
|
||||||
|
|
||||||
|
Presné lexikálne výsledky majú prednosť pred slabšími sémantickými
|
||||||
|
zhodami. Pri slabšej `any_term` stratégii sa znižuje jej váha a väčší
|
||||||
|
význam môže dostať embeddingové vyhľadávanie.
|
||||||
|
|
||||||
Test z terminálu:
|
Test z terminálu:
|
||||||
|
|
||||||
```bash
|
``` bash
|
||||||
docker compose run --rm zp-agent-api \
|
docker compose run --rm zp-agent-api python scripts/search_db.py "rag agent" --limit 5
|
||||||
python scripts/search_db.py "rag agent" --limit 5
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Pred volaním API načítaj premenné z `.env`:
|
Pred volaním API načítaj premenné z `.env`:
|
||||||
|
|
||||||
```bash
|
``` bash
|
||||||
set -a
|
set -a
|
||||||
source .env
|
source .env
|
||||||
set +a
|
set +a
|
||||||
@ -156,59 +248,197 @@ set +a
|
|||||||
|
|
||||||
Vyhľadávanie cez zabezpečené API:
|
Vyhľadávanie cez zabezpečené API:
|
||||||
|
|
||||||
```bash
|
``` bash
|
||||||
curl -X POST http://127.0.0.1:8000/search \
|
curl -X POST http://127.0.0.1:8000/search -H "Content-Type: application/json" -H "Authorization: Bearer $SEARCH_API_KEY" -d '{
|
||||||
-H "Content-Type: application/json" \
|
"query": "strojový preklad",
|
||||||
-H "X-API-Key: $SEARCH_API_KEY" \
|
"limit": 3,
|
||||||
-d '{
|
|
||||||
"query": "rag agent",
|
|
||||||
"limit": 5,
|
|
||||||
"published_only": false,
|
"published_only": false,
|
||||||
"max_per_document": 3
|
"max_per_document": 1
|
||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
API vracia hybridný engine:
|
API používa hybridný engine:
|
||||||
|
|
||||||
```text
|
``` text
|
||||||
hybrid_fts5_embeddings
|
hybrid_fts5_embeddings
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`max_per_document=1` zabezpečí, že sa pri požiadavke na viac výsledkov
|
||||||
|
preferujú rôzne dokumenty namiesto viacerých chunkov z rovnakého
|
||||||
|
dokumentu.
|
||||||
|
|
||||||
|
## RAG
|
||||||
|
|
||||||
|
Nad hybridným retrievalom je implementovaná RAG vrstva v
|
||||||
|
`scripts/rag_utils.py`.
|
||||||
|
|
||||||
|
Endpoint `/rag` vyhľadá relevantné dokumenty a pripraví:
|
||||||
|
|
||||||
|
- textový kontext pre jazykový model,
|
||||||
|
- metadata použitých zdrojov,
|
||||||
|
- názov a autora dokumentu,
|
||||||
|
- `source_url`,
|
||||||
|
- interné retrieval informácie,
|
||||||
|
- pravidlá pre grounded odpoveď.
|
||||||
|
|
||||||
|
RAG pravidlá požadujú, aby model odpovedal iba podľa poskytnutých
|
||||||
|
zdrojov. Zároveň rozlišujú údaje, ktoré sa môžu ľahko zameniť, napríklad
|
||||||
|
autora dokumentu, osobu, o ktorej dokument pojednáva, rok začiatku
|
||||||
|
štúdia a rok záverečnej práce.
|
||||||
|
|
||||||
|
Príklad:
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
curl -X POST http://127.0.0.1:8000/rag -H "Content-Type: application/json" -H "Authorization: Bearer $SEARCH_API_KEY" -d '{
|
||||||
|
"query": "V akom roku robil Ján Holp diplomovú prácu?",
|
||||||
|
"limit": 5,
|
||||||
|
"published_only": false,
|
||||||
|
"max_per_document": 1
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## OpenWebUI
|
||||||
|
|
||||||
|
ZP Agent je pripojený do OpenWebUI ako OpenAPI Tool Server.
|
||||||
|
|
||||||
|
OpenWebUI používa:
|
||||||
|
|
||||||
|
``` text
|
||||||
|
OpenAPI URL: http://localhost:8000/openapi.json
|
||||||
|
Authentication: Bearer
|
||||||
|
Token: SEARCH_API_KEY
|
||||||
|
```
|
||||||
|
|
||||||
|
V reálnom nasadení musí byť URL API dostupná z prostredia, v ktorom beží
|
||||||
|
OpenWebUI; `localhost` je vhodný iba vtedy, ak sa OpenWebUI pripája k
|
||||||
|
API z rovnakého hostiteľa.
|
||||||
|
|
||||||
|
OpenAPI schéma určená pre integráciu vystavuje RAG nástroj, ktorý môže
|
||||||
|
jazykový model použiť pri otázkach nad ZP Wiki.
|
||||||
|
|
||||||
|
Tok požiadavky:
|
||||||
|
|
||||||
|
``` text
|
||||||
|
používateľ
|
||||||
|
↓
|
||||||
|
OpenWebUI
|
||||||
|
↓
|
||||||
|
jazykový model
|
||||||
|
↓
|
||||||
|
ZP Agent /rag
|
||||||
|
↓
|
||||||
|
hybrid retrieval
|
||||||
|
↓
|
||||||
|
RAG kontext + zdroje
|
||||||
|
↓
|
||||||
|
jazykový model
|
||||||
|
↓
|
||||||
|
odpoveď
|
||||||
|
```
|
||||||
|
|
||||||
|
Príklad otázky:
|
||||||
|
|
||||||
|
``` text
|
||||||
|
Použi ZP Agent a zisti, v akom roku robil Ján Holp diplomovú prácu.
|
||||||
|
```
|
||||||
|
|
||||||
|
Výsledná odpoveď obsahuje stručnú informáciu získanú zo ZP Wiki a
|
||||||
|
príslušný `source_url`.
|
||||||
|
|
||||||
|
## Manuálna synchronizácia
|
||||||
|
|
||||||
Manuálne reindexovanie cez zabezpečený endpoint:
|
Manuálne reindexovanie cez zabezpečený endpoint:
|
||||||
|
|
||||||
```bash
|
``` bash
|
||||||
curl -X POST http://127.0.0.1:8000/sync \
|
curl -X POST http://127.0.0.1:8000/sync -H "Content-Type: application/json" -H "X-API-Key: $SYNC_API_KEY" -d '{"pull_git": false}'
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-H "X-API-Key: $SYNC_API_KEY" \
|
|
||||||
-d '{"pull_git": false}'
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Gitea webhook
|
||||||
|
|
||||||
|
Endpoint:
|
||||||
|
|
||||||
|
``` text
|
||||||
|
POST /webhook/gitea
|
||||||
|
```
|
||||||
|
|
||||||
|
Webhook overuje:
|
||||||
|
|
||||||
|
- HMAC-SHA256 podpis,
|
||||||
|
- typ Gitea udalosti,
|
||||||
|
- očakávaný repozitár,
|
||||||
|
- stav reindexovacieho zámku.
|
||||||
|
|
||||||
|
Očakávaný repozitár:
|
||||||
|
|
||||||
|
``` dotenv
|
||||||
|
EXPECTED_GITEA_REPOSITORY=KEMT/zpwiki
|
||||||
|
```
|
||||||
|
|
||||||
|
Ak je povolené:
|
||||||
|
|
||||||
|
``` dotenv
|
||||||
|
WEBHOOK_PULL_GIT=true
|
||||||
|
```
|
||||||
|
|
||||||
|
pred reindexovaním sa vykoná:
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
git pull --ff-only
|
||||||
|
```
|
||||||
|
|
||||||
|
## Bezpečnosť
|
||||||
|
|
||||||
|
Vyhľadávanie podporuje API key:
|
||||||
|
|
||||||
|
``` text
|
||||||
|
X-API-Key: <SEARCH_API_KEY>
|
||||||
|
```
|
||||||
|
|
||||||
|
aj Bearer autentifikáciu:
|
||||||
|
|
||||||
|
``` text
|
||||||
|
Authorization: Bearer <SEARCH_API_KEY>
|
||||||
|
```
|
||||||
|
|
||||||
|
Bearer autentifikácia sa používa najmä pri integrácii s OpenWebUI.
|
||||||
|
|
||||||
|
Endpoint `/sync` používa samostatný `SYNC_API_KEY` a webhook samostatný
|
||||||
|
`WEBHOOK_SECRET`.
|
||||||
|
|
||||||
|
Tajomstvá sa ukladajú do `.env`, ktorý nesmie byť súčasťou Git
|
||||||
|
repozitára.
|
||||||
|
|
||||||
## Testy
|
## Testy
|
||||||
|
|
||||||
Inštalácia testovacích závislostí:
|
Inštalácia testovacích závislostí:
|
||||||
|
|
||||||
```bash
|
``` bash
|
||||||
pip install -r requirements-dev.txt
|
pip install -r requirements-dev.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
Bežné automatizované testy:
|
Bežné automatizované testy:
|
||||||
|
|
||||||
```bash
|
``` bash
|
||||||
pytest -q test
|
pytest -q test
|
||||||
```
|
```
|
||||||
|
|
||||||
Aktuálna testovacia sada:
|
Aktuálny stav:
|
||||||
|
|
||||||
```text
|
``` text
|
||||||
65 passed, 2 skipped
|
77 passed, 2 skipped
|
||||||
```
|
```
|
||||||
|
|
||||||
Testy vrátane kontroly reálne vygenerovaných dát a databázy:
|
Testy vrátane kontroly reálnych dát a databázy:
|
||||||
|
|
||||||
```bash
|
``` bash
|
||||||
RUN_LIVE_TESTS=1 pytest -q test
|
RUN_LIVE_TESTS=1 pytest -q test
|
||||||
```
|
```
|
||||||
|
|
||||||
## Ďalší krok
|
Aktuálny stav:
|
||||||
|
|
||||||
Najbližšia etapa je integrácia s OpenWebUI a vytvorenie agentového rozhrania. Následne sa doplnia RAG odpovede so zdrojmi a citáciami.
|
``` text
|
||||||
|
79 passed
|
||||||
|
```
|
||||||
|
|
||||||
|
Testovacia sada pokrýva indexovanie, vyhľadávanie, hybridný retrieval,
|
||||||
|
API, autentifikáciu, RAG utility, RAG endpoint a OpenAPI integráciu.
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user