Initial commit: Hermes skill rag-pipeline-docker

This commit is contained in:
estorozhenko
2026-09-06 13:51:06 +00:00
commit bdc03a5d59
6 changed files with 656 additions and 0 deletions
+108
View File
@@ -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
+71
View File
@@ -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.
+28
View File
@@ -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 начнёт с чистого листа.