mirror of
https://gitverse.ru/kpa39l/rag-pipeline-docker.git
synced 2026-09-29 09:15:11 +00:00
Initial commit: Hermes skill rag-pipeline-docker
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
# Memory OS Deployment — Session Details
|
||||
|
||||
## Environment
|
||||
|
||||
- Host: Linux, no Docker Desktop
|
||||
- Ollama: Docker container on `ollama_default` network, port 11434
|
||||
- Worker: Docker Compose stack with Qdrant + Redis + ARQ worker
|
||||
- Obsidian vault: `/opt/hermes/obsidian-vault/`
|
||||
- Wiki path: `/opt/hermes/vault/wiki/raw/obsidian/`
|
||||
- State file: `~/.hermes/wiki_ingest_state.json` (tracks ingested files by hash)
|
||||
- Sync state: `~/.hermes/obsidian_sync_state.json`
|
||||
|
||||
## Docker Compose Path
|
||||
|
||||
`/opt/hermes/memory-os/docker/docker-compose.yml`
|
||||
|
||||
## .env File
|
||||
|
||||
`/opt/hermes/memory-os/docker/.env` — contains:
|
||||
- `OLLAMA_BASE_URL=http://ollama:11434`
|
||||
- `OLLAMA_MODEL=qwen3-8b-64k`
|
||||
- `OLLAMA_EMBEDDING_MODEL=nomic-embed-text:latest`
|
||||
- `EMBEDDING_API_BASE=http://ollama:11434/v1`
|
||||
- `EMBEDDING_DIMS=768`
|
||||
- `REDIS_PASSWORD`, `QDRANT_API_KEY`, `OPENROUTER_API_KEY`
|
||||
|
||||
## Error: Reflection Failing
|
||||
|
||||
**Symptom:** `j_failed=90` on worker, all from `cron:process_reflection`
|
||||
|
||||
**Error:**
|
||||
```
|
||||
httpx.ConnectError: [Errno -2] Name or service not known
|
||||
```
|
||||
The worker was trying `http://host.docker.internal:11434/api/generate`
|
||||
|
||||
**Root cause:** `OLLAMA_BASE_URL` defaulted to `http://host.docker.internal:11434` in `services/llm.py`:
|
||||
```python
|
||||
OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "http://host.docker.internal:11434")
|
||||
```
|
||||
This variable was not listed in `docker-compose.yml` under `worker.environment`, so the container fell back to the hardcoded default.
|
||||
|
||||
**Fix:**
|
||||
1. Added `OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-http://ollama:11434}` and `OLLAMA_MODEL: ${OLLAMA_MODEL:-qwen3-8b-64k}` to `docker-compose.yml`
|
||||
2. Added `OLLAMA_MODEL=qwen3-8b-64k` to `.env`
|
||||
3. Connected worker to `ollama_default` external network in compose
|
||||
4. `docker compose up -d --force-recreate worker` to rebuild
|
||||
|
||||
## Network Layout
|
||||
|
||||
```
|
||||
worker (memory-os_default: 172.24.0.x, ollama_default: 172.18.0.3)
|
||||
→ ollama (ollama_default: 172.18.0.5) via DNS
|
||||
→ qdrant (memory-os_default) via DNS
|
||||
→ redis (memory-os_default) via DNS
|
||||
search-api (memory-os_default: 172.24.0.x, ollama_default: 172.18.0.x)
|
||||
→ ollama (ollama_default) via DNS
|
||||
→ qdrant (memory-os_default) via DNS
|
||||
```
|
||||
|
||||
## Qdrant Collection
|
||||
|
||||
- Name: `knowledge_base`
|
||||
- Points: 683
|
||||
- Dense vector: 768 dims, Cosine distance
|
||||
- Sparse vector: BM25 (on_disk=True)
|
||||
- Embedding model: `nomic-embed-text:latest`
|
||||
|
||||
## Search API
|
||||
|
||||
**Path:** `/opt/hermes/memory-os/search_api/main.py`
|
||||
**Port:** localhost:8000
|
||||
**Docker image:** `docker-search-api` (built from `search_api/Dockerfile`)
|
||||
|
||||
**Files:**
|
||||
```
|
||||
search_api/
|
||||
├── Dockerfile
|
||||
├── requirements.txt # fastapi, uvicorn, httpx, pydantic
|
||||
└── main.py # FastAPI app with POST /search and GET /health
|
||||
```
|
||||
|
||||
**Endpoints:**
|
||||
- `GET /health` → `{"status": "ok", "qdrant": true, "ollama": true}`
|
||||
- `POST /search` → body: `{"query": "search text", "top_k": 3}` → `{"query": "...", "results": [...], "total": N}`
|
||||
|
||||
## Cron Jobs
|
||||
|
||||
### sync+ingest (LLM-driven, every 10m)
|
||||
- Name: `memory-os obsidian sync + ingest`
|
||||
- Schedule: every 10 minutes
|
||||
- Prompt: Runs sync script + ingest pipeline
|
||||
- Tools: terminal only
|
||||
|
||||
### micro-reflection trigger (no_agent, every 5m, silent)
|
||||
- Name: `memory-os micro-reflection trigger`
|
||||
- Schedule: `*/5 * * * *`
|
||||
- Script: `docker exec docker-worker-1 python3 /app/scripts/reflection_trigger.py`
|
||||
- `no_agent: true` — no LLM tokens, just runs the script
|
||||
- `deliver: local` — silent, no Telegram notifications
|
||||
- Script path must be mounted into container: `- ../scripts:/app/scripts:ro` in docker-compose.yml
|
||||
|
||||
## Scripts
|
||||
|
||||
- `scripts/test_qdrant_search.py` — standalone test: takes --query, gets embedding, searches Qdrant, prints top-5
|
||||
- `scripts/sync_obsidian_to_wiki.py` — copies .md from Obsidian vault to wiki path, tracks state
|
||||
- `scripts/wiki_continuous_ingest.py` — detects new/changed files, enqueues to ARQ worker
|
||||
- `scripts/reflection_trigger.py` — checks idle, respects budget, enqueues micro-reflection
|
||||
@@ -0,0 +1,71 @@
|
||||
# MTProto Proxy via alexbers/mtprotoproxy + Caddy TLS
|
||||
|
||||
## Отличие от seriyps/mtproto-proxy
|
||||
|
||||
Существующий скилл `mtproto-proxy` описывает образ `seriyps/mtproto-proxy` с Fake TLS и переменными `MTP_*`. Это **другой** образ. `alexbers/mtprotoproxy` использует:
|
||||
|
||||
- **Python-конфиг** (`config.py`) вместо переменных окружения
|
||||
- **Реальные TLS-сертификаты** (через nginx/Caddy reverse proxy) вместо Fake TLS
|
||||
- `network_mode: host` вместо bridge
|
||||
|
||||
## Docker Compose (с Caddy)
|
||||
|
||||
```yaml
|
||||
services:
|
||||
mtproto:
|
||||
image: alexbers/mtprotoproxy
|
||||
restart: always
|
||||
network_mode: host
|
||||
volumes:
|
||||
# Сертификаты от Caddy (LetsEncrypt)
|
||||
- /opt/caddy/caddy_data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/domain.ru:/certs:ro
|
||||
# Конфиг
|
||||
- ./mtproto:/config
|
||||
command: python3 mtprotoproxy.py /config/config.py
|
||||
```
|
||||
|
||||
## config.py
|
||||
|
||||
```python
|
||||
# MTProto proxy config
|
||||
PORT = 443 # or whatever port Caddy forwards TLS to
|
||||
USERS = {
|
||||
"tg": "ee" + "32-hex-chars-secret"
|
||||
}
|
||||
# Optional: stats reporting
|
||||
# SECRET = 123456 # for stats (unsafe, optional)
|
||||
```
|
||||
|
||||
## TLS via Caddy
|
||||
|
||||
Caddy reverse proxy ставится перед MTProto:
|
||||
|
||||
```yaml
|
||||
labels:
|
||||
caddy: domain.ru
|
||||
caddy.reverse_proxy: / "{{upstreams 443}}"
|
||||
caddy.reverse_proxy.transport: http
|
||||
caddy.reverse_proxy.transport.tls: "insecure_skip_verify"
|
||||
```
|
||||
|
||||
Caddy получает LetsEncrypt сертификаты и пробрасывает HTTPS-трафик на MTProto (который внутри слушает без TLS).
|
||||
|
||||
## Ссылка для подключения
|
||||
|
||||
```
|
||||
https://t.me/proxy?server=domain.ru&port=443&secret=eexxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
```
|
||||
|
||||
Секрет с префиксом `ee` — Telegram на клиенте сам определяет что это Fake TLS / реальный TLS.
|
||||
|
||||
## Медленное подключение (3+ минуты) — возможные причины
|
||||
|
||||
1. **DNS resolver на сервере** — MTProto прокси использует DNS для проверки Telegram API. Если DNS медленный или блокируется, задержка большая. Лечение: проверить `/etc/resolv.conf`, поставить `1.1.1.1` / `8.8.8.8`.
|
||||
|
||||
2. **Caddy появляется раньше MTProto** — если Caddy стартует быстрее, он выдаёт ошибку вместо прокси. Telegram клиент пытается переподключаться, что добавляет задержку. Лечение: настроить `depends_on` или restart политику.
|
||||
|
||||
3. **TCP keepalive** — MTProto держит соединения. Если между клиентом и сервером есть NAT с таймаутом меньше 3 минут, соединение разрывается и восстанавливается. Это может выглядеть как "3 минуты подключается".
|
||||
|
||||
4. **Медленная загрузка сертификатов** — если сертификаты лежат на WebDAV/Yandex Disk, mount может тормозить. Проверить `mount` и права доступа.
|
||||
|
||||
5. **Проверка:** внутри контейнера `docker exec mtproto cat /config/config.py`, снаружи `ss -tlnp | grep mtproto`.
|
||||
@@ -0,0 +1,81 @@
|
||||
# Qdrant: version matching + multiple collections (verified 2026-09-04)
|
||||
|
||||
## qdrant-client must match the server minor version
|
||||
|
||||
Symptom chain with client 1.19.0 vs server 1.17.1 (`qdrant/qdrant:v1.17.1`):
|
||||
- `Qdrant client version 1.19.0 is incompatible with server version 1.17.1` warning,
|
||||
- `create_collection` with a bare `models.VectorParams(size=1024, ...)` silently
|
||||
creates an ANONYMOUS vector (name `""`), NOT `dense`,
|
||||
- the subsequent `upsert` fails: `400 ... Not existing vector name error: dense`.
|
||||
|
||||
Fix — pin the client to the server minor:
|
||||
```bash
|
||||
pip install "qdrant-client==1.17.1" # match qdrant/qdrant:v1.17.1
|
||||
```
|
||||
|
||||
Always pass NAMED vectors so the config works regardless of client version:
|
||||
```python
|
||||
from qdrant_client import QdrantClient, models
|
||||
c = QdrantClient("http://localhost:6333")
|
||||
c.create_collection(
|
||||
collection_name=NAME,
|
||||
vectors_config={
|
||||
"dense": models.VectorParams(size=1024, distance=models.Distance.COSINE),
|
||||
},
|
||||
sparse_vectors_config={
|
||||
"sparse": models.SparseVectorParams(index=models.SparseIndexParams(on_disk=True)),
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
## Query API in qdrant-client 1.17
|
||||
|
||||
- `client.search(...)` does NOT exist in 1.17.
|
||||
- `query_points(..., query_vector=...)` → `AssertionError: Unknown arguments: ['query_vector']`.
|
||||
- Working call:
|
||||
```python
|
||||
res = c.query_points(
|
||||
collection_name=NAME,
|
||||
query=<dense_embedding_list>, # list[float] from Ollama /api/embeddings
|
||||
using="dense", # named-vector selector
|
||||
limit=5,
|
||||
with_payload=True,
|
||||
)
|
||||
for pt in res.points:
|
||||
print(pt.score, pt.payload.get("text"))
|
||||
```
|
||||
|
||||
## One collection per project/domain (multi-collection design)
|
||||
|
||||
For a distinct document set (batch of PDF protocol/files), create a SEPARATE
|
||||
collection with the SAME schema (`dense` 1024d COSINE + sparse `sparse` BM25) and the
|
||||
SAME embedder (bge-m3). Keep search query embeddings compatible by using the same
|
||||
embedder for all collections.
|
||||
Benefits: independent re-index, per-domain context search, no pollution of the
|
||||
general KB. Example: `skc_vinny_gorod` alongside `knowledge_base`.
|
||||
|
||||
## Do NOT retarget context_enhancer to a second collection
|
||||
|
||||
`context_enhancer.py` binds `COLLECTION = os.environ.get("QDRANT_COLLECTION",
|
||||
"knowledge_base")` at IMPORT time (module level). Swapping the env var at runtime
|
||||
does NOT retarget it — the module-level constant is already fixed.
|
||||
|
||||
To search a second collection, write a STANDALONE REST search:
|
||||
1. `POST http://ollama:11434/api/embeddings` `{"model": "bge-m3", "prompt": text}` → `embedding` (1024d).
|
||||
2. `POST http://qdrant:6333/collections/<NAME>/points/query` with `{"vector": emb, "limit": N, "with_payload": true, "using": "dense"}`.
|
||||
3. Read `result.points[*].payload` + `.score`.
|
||||
|
||||
This same pattern is the foundation for a per-project context-injector hook
|
||||
(e.g. NetBox project context) — hit the secondary collection directly, don't go
|
||||
through the shared KB search.
|
||||
|
||||
## Scanned PDFs: pymupdf returns empty text (no text layer)
|
||||
|
||||
`page.get_text("text")` returns `""` for image-only pages — verified on a real
|
||||
51-page government PDF (0 text on every page). It is a genuine scan, not a glitch.
|
||||
Mark scanned pages `[SCANNED_PAGE]` and route to OCR (marker-pdf / vision);
|
||||
do NOT report "no content" or fabricate text. pymupdf's built-in Type1 fonts
|
||||
(times-roman, helv, cour, tiro) do NOT contain the Cyrillic glyph map — inserting
|
||||
Cyrillic with them renders as dots and re-extracts as dots. Real PDFs (generated
|
||||
from Word/CAD) embed proper fonts and extract Cyrillic fine; the byte test above is
|
||||
only a pymupdf-font artifact, not a real-PDF problem.
|
||||
@@ -0,0 +1,28 @@
|
||||
# WebUI Session Cache Reset (Post-Agent-Update)
|
||||
|
||||
## Симптомы
|
||||
|
||||
После обновления Hermes agent (особенно 0.19.x → 0.20.x+):
|
||||
|
||||
- WebUI бесконечно показывает "Loading conversation..."
|
||||
- Создание нового чата не помогает
|
||||
- Даже ответ в существующем чате не рендерится
|
||||
- Формат сессий на диске изменился, старый фронтенд не может его отрендерить
|
||||
|
||||
## Фикс
|
||||
|
||||
```bash
|
||||
# 1. Сбросить кэш сессий внутри контейнера WebUI
|
||||
docker exec hermes-webui sh -c 'rm -rf /home/hermeswebui/.hermes/webui/sessions/*'
|
||||
|
||||
# 2. Перезапустить WebUI
|
||||
cd /opt/hermes/docker && docker compose restart hermes-webui
|
||||
|
||||
# 3. Открыть в браузере приватное/инкогнито окно (чтобы не было клиентского кэша)
|
||||
```
|
||||
|
||||
## Почему это происходит
|
||||
|
||||
Hermes WebUI — отдельный контейнер со своим образом. Когда агент обновляется (через `!hermes update`), сессии на диске могут изменить формат (новые поля, другая структура context_messages). WebUI-образ не обновляется автоматически — он остаётся старым и не умеет парсить новые сессии.
|
||||
|
||||
Радикальное решение: сбросить кэш старых сессий, WebUI начнёт с чистого листа.
|
||||
Reference in New Issue
Block a user