347 lines
15 KiB
Markdown
347 lines
15 KiB
Markdown
# Home AI Assistant
|
||
|
||
Домашний ИИ-ассистент с REST API, веб-интерфейсом и помидоро-таймером. LLM — OpenRouter (по умолчанию DeepSeek).
|
||
|
||
## Возможности
|
||
|
||
- Чат с потоковыми ответами (SSE), скриншоты и vision-разбор изображений
|
||
- Помидоро-таймер с циклом работа/перерывы, управление из чата (tool calling)
|
||
- Долгосрочная память, профиль пользователя, опциональный RAG (Qdrant)
|
||
- Фитнес: дневник, TDEE/Navy, графики веса и состава тела
|
||
- Списки покупок, календарь напоминаний
|
||
- Персонаж и генерация картинок (ComfyUI / RP Chat)
|
||
- Интеграции: Taiga, Gitea, погода, утренний дайджест, Netdata
|
||
- Мультипользовательская авторизация по API-токену (`/login`)
|
||
- Веб-интерфейс: Чат, Помидоро, Персонаж, Память, Фитнес, Покупки, Календарь, Настройки
|
||
- REST API для внешних клиентов — см. [Telegram-бот](telegram-bot/README.md)
|
||
|
||
## Быстрый старт
|
||
|
||
### 1. Настройка окружения
|
||
|
||
```bash
|
||
cp .env.example .env
|
||
```
|
||
|
||
Заполните в `.env`:
|
||
|
||
```env
|
||
OPENROUTER_API_KEY=sk-or-v1-...
|
||
BACKEND_PORT=8080
|
||
FRONTEND_PORT=3080
|
||
```
|
||
|
||
Если порт занят (например, 3000 уже используется Gitea), смените `FRONTEND_PORT` на свободный.
|
||
|
||
### 2. Запуск через Docker
|
||
|
||
```bash
|
||
docker compose up --build
|
||
```
|
||
|
||
- Backend API: http://localhost:${BACKEND_PORT:-8080}
|
||
- Web UI: http://localhost:${FRONTEND_PORT:-3080}
|
||
- Healthcheck: http://localhost:8080/api/v1/health
|
||
|
||
**Prod за nginx:** при загрузке скриншотов возможна ошибка `413 Request Entity Too Large` — дефолтный лимит nginx 1 MB. На **host nginx** (Ubuntu перед docker) добавьте `client_max_body_size 64m;` в `server { }` и в `location /api/`. Пример: [`deploy/nginx-host-assistant.conf.example`](deploy/nginx-host-assistant.conf.example). После правки: `sudo nginx -t && sudo systemctl reload nginx`. Контейнер frontend тоже поднимает лимит в `frontend/nginx.conf` — пересоберите образ.
|
||
|
||
Порты в `.env`:
|
||
|
||
| Переменная | По умолчанию | Назначение |
|
||
|------------|--------------|------------|
|
||
| `BACKEND_PORT` | 8080 | API с хоста |
|
||
| `FRONTEND_PORT` | 3080 | Веб-морда с хоста |
|
||
| `VITE_DEV_PORT` | 5173 | Frontend при `npm run dev` |
|
||
| `TAIGA_PORT` | 9000 | Taiga (фаза 2) |
|
||
| `GITEA_PORT` | 3000 | Gitea HTTP (фаза 2) |
|
||
| `GITEA_SSH_PORT` | 222 | Gitea SSH (фаза 2) |
|
||
| `QDRANT_PORT` | 6333 | Qdrant HTTP |
|
||
| `POSTGRES_USER` / `POSTGRES_PASSWORD` / `POSTGRES_DB` | assistant | PostgreSQL в docker compose |
|
||
|
||
### 3. Локальная разработка
|
||
|
||
**Backend:**
|
||
|
||
```bash
|
||
cd backend
|
||
python -m venv .venv
|
||
.venv\Scripts\activate # Windows
|
||
pip install -r requirements-dev.txt
|
||
uvicorn app.main:app --reload --port 8080
|
||
```
|
||
|
||
Для локального backend без Docker задайте в `.env`:
|
||
|
||
```env
|
||
DATABASE_URL=sqlite:///./data/assistant.db
|
||
```
|
||
|
||
**Frontend:**
|
||
|
||
```bash
|
||
cd frontend
|
||
npm install
|
||
npm run dev
|
||
```
|
||
|
||
Vite dev-server: http://localhost:5173 (проксирует `/api` на backend).
|
||
|
||
## REST API
|
||
|
||
Полная схема — Swagger UI: `http://localhost:${BACKEND_PORT:-8080}/docs`
|
||
|
||
Основные эндпоинты (префикс `/api/v1`, авторизация `Authorization: Bearer <token>` если `AUTH_REQUIRED=true`):
|
||
|
||
| Method | Path | Описание |
|
||
|--------|------|----------|
|
||
| POST | `/login` | Получить сессию по API-токену |
|
||
| GET | `/health` | Healthcheck |
|
||
| POST/GET | `/chat/sessions` | Чат-сессии и история |
|
||
| POST | `/chat/sessions/{id}/messages` | Сообщение (SSE) |
|
||
| GET/POST | `/pomodoro/*` | Таймер |
|
||
| GET/PUT | `/memory`, `/profile` | Память и профиль |
|
||
| GET/POST | `/fitness/*` | Фитнес, графики `/fitness/charts` |
|
||
| GET/POST | `/shopping/*` | Списки покупок |
|
||
| GET/POST | `/reminders/*` | Напоминания и календарь |
|
||
| GET/POST | `/documents/*` | Загрузка документов (RAG) |
|
||
| GET | `/homelab/status`, `/homelab/weather` | Homelab |
|
||
| GET/PUT | `/settings` | RAG toggle, пользователи |
|
||
| GET/POST | `/projects`, `/work-items` | Taiga + Gitea |
|
||
| POST | `/webhooks/gitea` | Webhook автозакрытия |
|
||
|
||
## Taiga + Gitea (фаза 2)
|
||
|
||
Taiga и Gitea обычно работают **на хосте** (не в Docker compose). Контейнер backend достучится через `host.docker.internal` (настроено в `docker-compose.yml`). Публичные URL — в `.env` (`TAIGA_PUBLIC_URL`, `GITEA_PUBLIC_URL`).
|
||
|
||
### Настройка `.env`
|
||
|
||
```env
|
||
TAIGA_BASE_URL=http://host.docker.internal:9000
|
||
TAIGA_USERNAME=...
|
||
TAIGA_PASSWORD=...
|
||
TAIGA_PUBLIC_URL=https://taiga.example.com
|
||
|
||
GITEA_BASE_URL=http://host.docker.internal:3000
|
||
GITEA_TOKEN=... # Settings → Applications → Generate Token
|
||
GITEA_PUBLIC_URL=https://git.example.com
|
||
GITEA_WEBHOOK_SECRET=... # произвольная строка
|
||
```
|
||
|
||
### Первый запуск
|
||
|
||
```bash
|
||
# 1. Синхронизировать проекты Taiga (ID подтянутся автоматически)
|
||
curl -X POST http://localhost:8080/api/v1/projects/sync-taiga
|
||
|
||
# 2. Привязать Gitea repo к проекту Taiga
|
||
curl -X PUT http://localhost:8080/api/v1/projects/home-assistant/gitea \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"gitea_owner":"Grigo","gitea_repo":"Home_assistant","default_branch":"main"}'
|
||
```
|
||
|
||
### Gitea webhook
|
||
|
||
В репозитории: **Settings → Webhooks → Add Webhook**:
|
||
|
||
- URL (выбери один вариант):
|
||
- **Рекомендуется:** `https://assistant.example.com/api/v1/webhooks/gitea` — nginx → `127.0.0.1:${BACKEND_PORT}`
|
||
- **Если Gitea в Docker:** `http://172.17.0.1:${BACKEND_PORT}/api/v1/webhooks/gitea` — не `127.0.0.1` (это localhost контейнера Gitea)
|
||
- Content type: `application/json`
|
||
- Secret: значение `GITEA_WEBHOOK_SECRET`
|
||
- Events: **Push**
|
||
|
||
Проверка из контейнера Gitea: `docker exec gitea wget -qO- http://172.17.0.1:8202/api/v1/health`
|
||
Test delivery в Gitea должен вернуть **200**, не **0**.
|
||
|
||
### Автозакрытие по коммиту
|
||
|
||
В сообщении коммита:
|
||
|
||
```
|
||
fix: кнопка сохранения
|
||
Closes gitea #12, taiga #45
|
||
```
|
||
|
||
Закроются Gitea issue #12 и Taiga story #45 (если только один ref — второй найдётся по связи в БД).
|
||
|
||
### Чат
|
||
|
||
«Заведи баг: кнопка не сохраняет настройки» → `create_work_item` → Taiga story + Gitea issue + ветка `feature/45-...`.
|
||
|
||
## Структура проекта
|
||
|
||
```
|
||
backend/ FastAPI, OpenRouter, PostgreSQL (docker) / SQLite (local dev)
|
||
frontend/ React + Vite
|
||
telegram-bot/ Telegram-клиент (отдельный VPS)
|
||
data/ uploads, generated media, SQLite-бэкап при миграции
|
||
deploy/ примеры nginx
|
||
```
|
||
|
||
## PostgreSQL
|
||
|
||
В `docker compose` по умолчанию поднимается **PostgreSQL 16**. Переменные — `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB` в `.env`.
|
||
|
||
### Миграция с SQLite
|
||
|
||
Если уже есть `./data/assistant.db`:
|
||
|
||
```bash
|
||
# 1. Бэкап
|
||
cp -a data data.bak.$(date +%Y%m%d)
|
||
|
||
# 2. Поднять postgres и пересобрать backend (скрипт миграции в образе)
|
||
docker compose up -d postgres
|
||
docker compose build backend
|
||
|
||
# 3. Dry-run (подсчёт строк)
|
||
docker compose run --rm backend python scripts/migrate_sqlite_to_postgres.py --dry-run
|
||
|
||
# 4. Импорт (DATABASE_URL уже указывает на postgres в compose)
|
||
docker compose run --rm backend python scripts/migrate_sqlite_to_postgres.py
|
||
|
||
# 5. Перезапуск
|
||
docker compose up -d
|
||
```
|
||
|
||
Флаги: `--force` — очистить Postgres перед импортом; `--sqlite-path` — путь к файлу.
|
||
SQLite-файл **не удаляется** — остаётся бэкапом.
|
||
|
||
## Память и контекст
|
||
|
||
Долгосрочная память в БД (PostgreSQL или SQLite). При включённом RAG — семантический поиск фактов через Qdrant.
|
||
|
||
| Слой | Что хранит |
|
||
|------|------------|
|
||
| **Профиль** | имя, timezone, language, notes |
|
||
| **Факты** | устойчивые знания с категорией и важностью |
|
||
| **Сводка чата** | краткое содержание длинной сессии |
|
||
|
||
В system prompt на каждый ответ: персонаж → **время** → память → фитнес → **погода** → помидоро → проекты.
|
||
История чата обрезается до 40 последних сообщений; раннее — в `session_summaries`.
|
||
|
||
**Автоизвлечение:** после каждого ответа LLM анализирует ход диалога и сохраняет
|
||
устойчивые факты (`source=auto`). Отключить: `MEMORY_AUTO_EXTRACT=false`.
|
||
|
||
**UI:** вкладка `/memory` — профиль, факты, JSON-снимок для отладки.
|
||
|
||
### Tools
|
||
|
||
- `remember_fact` — «запомни, что…»
|
||
- `recall_memories` — поиск по памяти
|
||
- `forget_memory` — удалить факт по id
|
||
- `update_profile` — имя, часовой пояс и т.д.
|
||
- `update_session_summary` — сжать тему длинного чата
|
||
|
||
### API
|
||
|
||
| Method | Path | Описание |
|
||
|--------|------|----------|
|
||
| GET | `/api/v1/memory` | снимок памяти (+ `?session_id=`) |
|
||
| GET/PUT | `/api/v1/profile` | профиль |
|
||
| GET/POST | `/api/v1/memory/facts` | список / создать факт |
|
||
| DELETE | `/api/v1/memory/facts/{id}` | забыть |
|
||
| PUT | `/api/v1/memory/sessions/{id}/summary` | сводка чата |
|
||
|
||
## RAG (Qdrant)
|
||
|
||
Векторный поиск по фактам памяти и загруженным документам. **Два условия одновременно:**
|
||
|
||
1. `RAG_ENABLED=true` в `.env` (Qdrant должен быть доступен)
|
||
2. Toggle «RAG включён» в **Настройки** (веб-UI)
|
||
|
||
Загрузка документов: Settings → документы, или `POST /api/v1/documents/upload` (текст: `.txt`, `.md`, `.json`, `.csv`).
|
||
Tool в чате: `search_documents`.
|
||
|
||
Backfill существующих фактов:
|
||
|
||
```bash
|
||
docker compose exec backend python -m app.rag.migrate_memory_to_qdrant
|
||
```
|
||
|
||
Ограничения: нет API удаления документов; session summaries индексируются, но в чате читаются из SQLite; при ошибке embedding — fallback на топ фактов из БД.
|
||
|
||
## Фитнес-трекер
|
||
|
||
Профиль, дневник (еда/вода/вес/шаги/тренировки), калькуляторы TDEE и Navy (WHR/LBM/FFMI),
|
||
графики веса и состава тела (`/fitness`, API `/fitness/charts`), LLM-оценка ккал/БЖУ,
|
||
lookup wger + Open Food Facts, vision-импорт скриншотов Mi Fitness, напоминания в чат.
|
||
|
||
Чат: «обед: гречка 200г, курица 150г», «выпил 300 мл воды», «жим 80×5×3».
|
||
|
||
## Списки покупок
|
||
|
||
Несколько списков, позиции с количеством, отметка «куплено». Вкладка `/shopping`, tools в чате (`add_shopping_items`, `list_shopping_lists`, …).
|
||
|
||
Чат: «добавь молоко и хлеб в продукты», «что в списке покупок», «отметь молоко купленным».
|
||
|
||
## Homelab API (фаза 4)
|
||
|
||
Интеграции с домашней инфраструктурой:
|
||
|
||
| Сервис | URL по умолчанию | Назначение |
|
||
|--------|------------------|------------|
|
||
| Open-Meteo | `$OPENMETEO_BASE_URL` | Погода в контексте и tool `get_weather` |
|
||
| ComfyUI | `$COMFYUI_BASE_URL` | fallback / рофл-watcher |
|
||
| RP Chat (aiChatBot) | `http://host.docker.internal:8201` | `generate_image`: sd-prompt + Anima; appearance в `/character` |
|
||
| Netdata | `http://host.docker.internal:19999` | Алерты warning/critical → notice в чат |
|
||
|
||
**Утренний дайджест** (`MORNING_DIGEST_HOUR=8`): погода + RSS (Habr, r/programming по умолчанию).
|
||
По запросу: «что на улице», «будет ли дождь» → `get_weather`; полный брифинг → `get_morning_briefing`.
|
||
|
||
Переменные — в `.env.example` (секция Homelab).
|
||
|
||
### Проверка доступности
|
||
|
||
В образе backend нет `curl`/`wget`. Удобнее всего — API-диагностика (из контейнера или с хоста):
|
||
|
||
```bash
|
||
curl -s http://localhost:${BACKEND_PORT:-8202}/api/v1/homelab/status | python3 -m json.tool
|
||
```
|
||
|
||
Или изнутри backend через Python:
|
||
|
||
```bash
|
||
docker compose exec backend python -c "
|
||
import os, httpx
|
||
for url in [
|
||
os.environ.get('OPENMETEO_BASE_URL', 'http://host.docker.internal:8085').rstrip('/') + '/v1/forecast?latitude=59.93&longitude=30.33¤t=temperature_2m',
|
||
os.environ.get('COMFYUI_BASE_URL', 'http://host.docker.internal:8188').rstrip('/') + '/system_stats',
|
||
'http://host.docker.internal:19999/api/v1/info',
|
||
]:
|
||
try:
|
||
r = httpx.get(url, timeout=10)
|
||
print(url, '->', r.status_code, r.text[:120])
|
||
except Exception as e:
|
||
print(url, '-> ERROR', e)
|
||
"
|
||
```
|
||
|
||
По умолчанию **Anima** (как в aiChatBot): `COMFYUI_UNET` + `COMFYUI_CLIP` + `COMFYUI_VAE` + style LoRA.
|
||
`COMFYUI_CHECKPOINT` оставь пустым. Для SD1.5/Pony — укажи checkpoint и очисти `COMFYUI_UNET`.
|
||
|
||
## Telegram-бот
|
||
|
||
Отдельный сервис в [`telegram-bot/`](telegram-bot/README.md): диалог с ассистентом с VPS, привязка API-токена, дублирование notice из чата.
|
||
Создание пользователя: Settings → Пользователи или `docker compose exec backend python -m scripts.create_user`.
|
||
|
||
## Разработка и тесты
|
||
|
||
```bash
|
||
cd backend
|
||
pip install -r requirements-dev.txt
|
||
pytest tests/ -q
|
||
```
|
||
|
||
Перед деплоем Jenkins запускает pytest (см. `Jenkinsfile`). CI на GitHub Actions нет — только Jenkins на linux-ноде.
|
||
|
||
## Следующие фазы
|
||
|
||
- Taiga/fitness в утреннем дайджесте
|
||
- LLM-мотивация в фитнес-напоминаниях (сейчас шаблонные строки)
|
||
- API удаления документов и re-index при включении RAG задним числом
|
||
|
||
## Модель
|
||
|
||
По умолчанию: `deepseek/deepseek-chat` через OpenRouter. Альтернатива для болтовни: `google/gemini-2.0-flash`.
|