372 lines
21 KiB
Markdown
372 lines
21 KiB
Markdown
# Content-atom: Implementation Plan
|
||
|
||
> **For Hermes:** roadmap-план по этапам (MVP → V2 → V3). Реализация — по этапам, не все сразу.
|
||
|
||
**Goal:** Атомарный контент-конвейер — атом смысла как источник правды, каналы переупаковывают атомы вместо переписывания статей.
|
||
|
||
**Architecture:** Все стадии работают с `atom_id` вместо `articleIds`. `extract` разбирает статью на типизированные атомы, все 6 слоёв editor-pro привязываются к атому, `atom_versions` переупаковывают атом под каналы, `compositions` собирают выходы, `panel` показывает дашборд смысла.
|
||
|
||
**Tech Stack:** Python (editor-pro, neurogeo), TypeScript (content-pipeline, panel), Qdrant (эмбеддинги), Memgraph/AGE (граф, опционально), FastAPI (панель), Qwen 35B (локальный).
|
||
|
||
---
|
||
|
||
## Контекст и предположения
|
||
|
||
### Что уже есть
|
||
- **editor-pro** — 6-слойный конвейер (typo → LLM → enrich → GEO → interlink → astro-seo), работает со статьёй. Стек: Python, spaCy, httpx, neo4j (Memgraph), PennyLane (QAOA). Модель: Qwen 35B.
|
||
- **NeuroGEO** — GEO-скоринг целой статьи, FastAPI на :8009.
|
||
- **content-pipeline** — TS-оркестратор (`generate → optimize → publish`) + CMS-слой для управления публикациями. Работает с объектом `Article`.
|
||
- **Qdrant** — эмбеддинги уже используются (qdrant_save.py).
|
||
- **Memgraph** — для интрелинки.
|
||
|
||
### Что меняем
|
||
- Article → atom. Новая стадия `extract` в начале конвейера.
|
||
- Все стадии привязываются к `atom_id` вместо `articleIds`.
|
||
- `atom_versions` — переупаковка одного атома под каналы.
|
||
- `compositions` — сборка выходов (статья/TG/видео).
|
||
- `panel` — дашборд смысла вместо списка статей.
|
||
|
||
### Что НЕ тащим сразу
|
||
- Postgres + pgvector — Qdrant для эмбеддингов, атомы в том, что уже стоит.
|
||
- Graph (Memgraph/AGE) — только V4, когда атомы отработают.
|
||
- Адаптеры TG/видео — V3, когда web-канал стабилен.
|
||
|
||
---
|
||
|
||
## Дорожная карта
|
||
|
||
### Этап 1 — MVP: атомы + переупаковка (web-канал)
|
||
**Статус:** ✅ РЕАЛИЗОВАНО (2026-09-14)
|
||
**Цель:** `extract` разбирает статью на атомы, `atom_versions` переупаковывают под web.
|
||
|
||
- **atoms** — типизированные атомы (claim/fact/stat/qa/comparison/hook/cta и др.), body по типам (JSONB).
|
||
- **atom_versions** — версии под каналы (web_article | telegram | youtube_script | podcast | newsletter | service_card).
|
||
- **extract** — стадия: статья → типизированные атомы.
|
||
- **jobs/runs** — задачи пайплайна + стоимость/качество на каждый запуск.
|
||
- **Qdrant** — эмбеддинги атомов для дедупликации.
|
||
|
||
**KPI:** 4 домена (orabota, ipoteka-ufa, ufa-kvartira, rabota-ufa), один канал (web).
|
||
|
||
**Реализовано:**
|
||
- `atom-db` (Postgres 16 + pgvector) — Docker, автозапуск, порт 5434.
|
||
- `db/schema.sql` — 5 таблиц (projects, atoms, atom_versions, jobs, runs).
|
||
- `stage1/extract_to_postgres.py` — 387 атомов → Postgres за 0.4с, 387 atom_versions под web, 387 эмбеддингов в Qdrant за 11.6с.
|
||
- atom_id = UUID (Postgres PRIMARY KEY).
|
||
- README-stage1.md — инструкция по запуску.
|
||
|
||
### Этап 2 — связи + композиции (второй канал: Telegram)
|
||
**Цель:** дедупликация между доменами, сборка выходов.
|
||
|
||
- **atom_relations** — связи (supports/contradicts/expands/example_of/part_of/derived_from).
|
||
- **compositions** — рецепты сборки (типы, темы, порядок), atom_ids.
|
||
- **prompts** — промпты как данные (версионирование, A/B).
|
||
- **Дедупликация** — embedding + atom_relations вместо дублирования статей.
|
||
|
||
**KPI:** 2 канала (web + Telegram), дедup между доменами.
|
||
|
||
### Этап 3 — медиа + метрики (видео, подкаст, рассылка)
|
||
**Статус:** ✅ РЕАЛИЗОВАНО (2026-09-14)
|
||
**Цель:** сквозная аналитика от смысла до метрики.
|
||
|
||
- **assets** — медиа-активы (image | audio | video | subtitle), транскрипты.
|
||
- **atom_assets** — привязка атомов к медиа (таймкоды).
|
||
- **compositions** — video-сборка (recipe + atom_ids).
|
||
- **publications** — публикации + метрики (views/clicks/conversions/revenue).
|
||
- **media_assets.py** — сборка: stat/claim/qa → video → atom_assets → compositions → publications.
|
||
- 3 канала: video (youtube), podcast (audio/RSS), newsletter (email).
|
||
- Идемпотентно: чистит assets/atom_assets проекта перед вставкой; публикации не дублируются.
|
||
- Синхронизация: `python3 stage3/media_assets.py --all` (все проекты) или `--project rabota-ufa`.
|
||
|
||
**KPI:** 4+ канала, сквозная аналитика.
|
||
|
||
### Этап 4 — граф (Apache AGE)
|
||
**Статус:** ✅ РЕАЛИЗОВАНО (2026-09-14)
|
||
**Цель:** обходы и анализ связей через Cypher.
|
||
|
||
- Проекция: Atom (label=Atom, property=atom_id).
|
||
- graph_outbox — atom_relations (Postgres) → AGE через cypher() (OpenCypher).
|
||
- Тело атома НЕ хранится — только id + type (минимальные атрибуты).
|
||
- Синхронизация: `python3 stage4/graph_outbox.py --sync` (идемпотентно — DETACH DELETE + CREATE).
|
||
- Обход связей: `python3 stage4/graph_outbox.py --query` (MATCH по atom_id).
|
||
|
||
**Ключевые факты AGE:**
|
||
- `SET search_path TO ag_catalog;` перед каждым cypher()-запросом.
|
||
- Cypher: `CREATE`/`MATCH`/`RETURN` вместо устаревших `vertices()`/`edges()`.
|
||
- `cypher('graph', $$ ... $$) AS (v agtype)` — `$$` в SQL-файле, не в shell.
|
||
- Тестировать запросы: `docker exec -i age-db psql -U atom -d content_atom < file.cypher`.
|
||
- AGE_PASSWORD в контейнере = `agepass123` (не atompass123).
|
||
|
||
**KPI:** граф-проекция, обходы связей через Cypher.
|
||
|
||
### Этап 5 — панель (дашборд смысла)
|
||
**Цель:** управление атомами вместо статей.
|
||
|
||
- Дашборд: атомы по доменам, статусы (draft → enriched → edited → approved → published), версии под каналы.
|
||
- Стоимость на атом + runs — юнит-экономика по домену.
|
||
- Дедупликация по embedding.
|
||
- Composition builder — сборка выходов.
|
||
|
||
**KPI:** панель управляет атомами, юнит-экономика по домену.
|
||
|
||
---
|
||
|
||
## Схемы данных
|
||
|
||
### Атомы
|
||
```sql
|
||
CREATE TABLE atoms (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
project_id UUID REFERENCES projects(id) ON DELETE CASCADE,
|
||
type TEXT NOT NULL, -- claim | fact | stat | quote | definition | instruction | case | comparison | qa | opinion | hook | transition | cta | media_ref
|
||
status TEXT NOT NULL DEFAULT 'draft', -- draft | enriched | edited | approved | published | archived
|
||
title TEXT,
|
||
summary TEXT,
|
||
body JSONB NOT NULL, -- типизированное тело по типу атома
|
||
plain_text TEXT,
|
||
language TEXT DEFAULT 'ru',
|
||
entities JSONB DEFAULT '[]',
|
||
topics TEXT[] DEFAULT '{}',
|
||
intent TEXT,
|
||
audience TEXT,
|
||
confidence NUMERIC(3,2),
|
||
quality_score NUMERIC(4,2),
|
||
readability NUMERIC(5,2),
|
||
word_count INT,
|
||
source_refs JSONB DEFAULT '[]',
|
||
embedding VECTOR(1024),
|
||
generator JSONB,
|
||
cost JSONB,
|
||
pipeline_stage TEXT,
|
||
parent_atom_id UUID,
|
||
lineage_root_id UUID,
|
||
created_at TIMESTAMPTZ DEFAULT now(),
|
||
updated_at TIMESTAMPTZ DEFAULT now()
|
||
);
|
||
```
|
||
|
||
### atom_versions
|
||
```sql
|
||
CREATE TABLE atom_versions (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
atom_id UUID REFERENCES atoms(id) ON DELETE CASCADE,
|
||
channel TEXT NOT NULL, -- web_article | telegram | youtube_script | podcast | newsletter | service_card
|
||
format TEXT NOT NULL, -- markdown | html | plain | srt | ssml | json
|
||
content TEXT NOT NULL,
|
||
meta JSONB DEFAULT '{}',
|
||
quality_score NUMERIC(4,2),
|
||
created_at TIMESTAMPTZ DEFAULT now(),
|
||
UNIQUE (atom_id, channel, format)
|
||
);
|
||
```
|
||
|
||
### atom_relations
|
||
```sql
|
||
CREATE TABLE atom_relations (
|
||
from_id UUID REFERENCES atoms(id) ON DELETE CASCADE,
|
||
to_id UUID REFERENCES atoms(id) ON DELETE CASCADE,
|
||
kind TEXT NOT NULL, -- supports | contradicts | expands | example_of | part_of | refutes | precedes | derived_from
|
||
weight NUMERIC(3,2) DEFAULT 1.0,
|
||
meta JSONB DEFAULT '{}',
|
||
PRIMARY KEY (from_id, to_id, kind)
|
||
);
|
||
```
|
||
|
||
### compositions
|
||
```sql
|
||
CREATE TABLE compositions (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
project_id UUID REFERENCES projects(id) ON DELETE CASCADE,
|
||
channel TEXT NOT NULL,
|
||
slug TEXT,
|
||
title TEXT,
|
||
status TEXT DEFAULT 'draft', -- draft | building | ready | scheduled | published
|
||
recipe JSONB NOT NULL, -- правила сборки: типы, темы, порядок
|
||
atom_ids UUID[] NOT NULL, -- порядок атомов в композиции
|
||
content TEXT,
|
||
meta JSONB DEFAULT '{}',
|
||
scheduled_at TIMESTAMPTZ,
|
||
published_at TIMESTAMPTZ,
|
||
created_at TIMESTAMPTZ DEFAULT now(),
|
||
UNIQUE (project_id, channel, slug)
|
||
);
|
||
```
|
||
|
||
### publications
|
||
```sql
|
||
CREATE TABLE publications (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
composition_id UUID REFERENCES compositions(id) ON DELETE CASCADE,
|
||
target TEXT NOT NULL, -- URL, channel_id, video_id
|
||
external_id TEXT,
|
||
published_at TIMESTAMPTZ DEFAULT now(),
|
||
metrics JSONB DEFAULT '{}' -- views, clicks, conversions, revenue
|
||
);
|
||
```
|
||
|
||
### jobs + runs
|
||
```sql
|
||
CREATE TABLE jobs (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
kind TEXT NOT NULL, -- generate | edit | adapt | publish | index
|
||
atom_id UUID REFERENCES atoms(id),
|
||
composition_id UUID REFERENCES compositions(id),
|
||
status TEXT DEFAULT 'queued',-- queued | running | done | failed | dead
|
||
attempts INT DEFAULT 0,
|
||
max_attempts INT DEFAULT 3,
|
||
payload JSONB DEFAULT '{}',
|
||
error TEXT,
|
||
scheduled_at TIMESTAMPTZ DEFAULT now(),
|
||
started_at TIMESTAMPTZ,
|
||
finished_at TIMESTAMPTZ,
|
||
created_at TIMESTAMPTZ DEFAULT now()
|
||
);
|
||
|
||
CREATE TABLE runs (
|
||
id BIGSERIAL PRIMARY KEY,
|
||
job_id UUID REFERENCES jobs(id) ON DELETE CASCADE,
|
||
stage TEXT NOT NULL,
|
||
model TEXT,
|
||
prompt_id UUID REFERENCES prompts(id),
|
||
tokens_in INT,
|
||
tokens_out INT,
|
||
seconds NUMERIC,
|
||
cost_usd NUMERIC DEFAULT 0,
|
||
quality NUMERIC(4,2),
|
||
created_at TIMESTAMPTZ DEFAULT now()
|
||
);
|
||
```
|
||
|
||
---
|
||
|
||
## Интеграция с текущими сервисами
|
||
|
||
### editor-pro (все 6 слоёв)
|
||
- Привязать каждый слой к `atom_id` вместо обработки статьи целиком.
|
||
- `extract` — новая стадия: `python -m editor_pro.main --extract` → атомы.
|
||
- Все слои работают с кластером атомов, не с одной статьёй.
|
||
|
||
### neurogeo
|
||
- GEO-скоринг на кластере атомов, не на статье.
|
||
- Цитируемость на атоме (fact/stat/claim).
|
||
|
||
### content-pipeline (TS)
|
||
- Новый шаг `extract` в `DEFAULT_PIPELINE`.
|
||
- `atomId` вместо `articleIds`.
|
||
- `atom_versions` — переупаковка под каналы.
|
||
|
||
### Qdrant
|
||
- Эмбеддинги атомов (уже используется).
|
||
- Дедупликация по embedding + atom_relations.
|
||
|
||
### Memgraph (V4)
|
||
- Проекция: Atom → Topic → Entity → Project.
|
||
- graph_outbox: Postgres → воркер → Memgraph.
|
||
|
||
---
|
||
|
||
## Панель (дашборд смысла)
|
||
|
||
**Стек:** FastAPI + SQLite/Postgres + React/Vue (или Astro для единого со стэком).
|
||
|
||
**Разделы:**
|
||
- Атомы по доменам (orabota, ipoteka-ufa, ufa-kvartira, rabota-ufa).
|
||
- Статусы атомов (draft → enriched → edited → approved → published).
|
||
- Версии под каналы (web/TG/видео).
|
||
- Стоимость на атом + runs — юнит-экономика по домену.
|
||
- Дедупликация по embedding.
|
||
- Composition builder — сборка выходов.
|
||
|
||
**KPI:** панель управляет атомами, юнит-экономика по домену.
|
||
|
||
---
|
||
|
||
## Правила
|
||
|
||
- Каждая стадия не зависит от предыдущих напрямую — только через атом.
|
||
- Промпты тянутся из `prompts` по `slug + version`.
|
||
- Все запуски пишутся в `runs` — воспроизводимость и аналитика.
|
||
- Идемпотентность через `(atom_id, stage, prompt_id, input_hash)`.
|
||
- Ошибки → `jobs.error`, ретраи с экспоненциальной задержкой.
|
||
- После `max_attempts` — статус `dead`, требует ручного вмешательства.
|
||
|
||
---
|
||
|
||
## Антипаттерны (из концепции)
|
||
|
||
- Хранить готовый HTML в атоме.
|
||
- Перезаписывать атом под канал.
|
||
- Промпты в коде, а не в БД.
|
||
- Один промпт на все проекты.
|
||
- Масштабировать на 50 доменов до юнит-экономики.
|
||
- Граф без ребилда из Postgres.
|
||
- Публикация без `compositions` (нельзя пересобрать).
|
||
- Метрики только на уровне сайта, не атома/композиции.
|
||
|
||
---
|
||
|
||
## Что даёт такая архитектура
|
||
|
||
| Возможность | Как достигается |
|
||
|---|---|
|
||
| Мультиканальность без переписывания | `atom_versions` + `compositions` |
|
||
| Дедупликация | `embedding` + `atom_relations` |
|
||
| Полное покрытие темы | граф связей + рецепты сборки |
|
||
| Сквозная аналитика | `publications.metrics` + `runs` |
|
||
| Воспроизводимость | `prompts` как данные + `runs` |
|
||
| Масштабирование на N доменов | `projects` + общий движок |
|
||
| Избежание footprint | разные шаблоны, ритмы, промпты |
|
||
| Стоимость и юнит-экономика | `cost` на атом + `runs` |
|
||
| Защита от обновлений | обновляешь атом → все производные пересобираются |
|
||
| Продажа частями | композиции и проекты изолированы |
|
||
|
||
---
|
||
|
||
## Решения (зафиксированы 2026-09-14)
|
||
|
||
1. **Хранение атомов:** Postgres (истина) + Qdrant (эмбеддинги). Postgres уже в Docker — один чистый инстанс `atom-db` с `pgvector` + `pg_trgm`. Атомы/версии/композиции/стоимость — в Postgres. Эмбеддинги + дедупликация — в Qdrant.
|
||
2. **Панель:** отложить до V5.
|
||
3. **V4 граф:** Apache AGE (расширение Postgres), не Memgraph. GraphOutbox/воркер не нужны.
|
||
4. **Эмбеддинги:** bge-m3 — единственная модель для MVP и продакшена (установлен в Ollama 2026-09-14, dim=1024, 8192 контекст, мультязычный, гибридный dense+sparse). Не переключаться между nomic/bge-m3 — разный размер вектора (512 vs 1024) и разная система поиска = переиндексация и баги дедупа.
|
||
5. **Домены:** 50 доменов, разделение по `role` (flagship/satellite/experiment/service). Технические домены (`service`) НЕ индексируются в семантику — дедуп только между SEO-доменами. Атомы привязаны к `project_id`, сборка — в рамках одного домена (защита footprint).
|
||
6. **Postgres:** чистый инстанс `atom-db` (не использовать outline-postgres/coolify-db — они в продакшене). `pgvector` + `pg_trgm` + `btree_gin`.
|
||
|
||
---
|
||
|
||
## Риски и как их избежать (проактивно, 2026-09-14)
|
||
|
||
### Риск 1: Разный размер вектора → переиндексация Qdrant
|
||
**Проблема:** bge-m3 = dim 1024, nomic = dim 512. Если в MVP использовать nomic, а потом перейти на bge-m3 — коллекция Qdrant пересоздаётся, все эмбеддинги переиндексируются.
|
||
**Решение:** сразу bge-m3 (dim=1024). Размер коллекции Qdrant фиксируется на старте. Проверить: `curl http://127.0.0.1:6333/collection/<name>` → `vector_size` должен быть 1024.
|
||
|
||
### Риск 2: Postgres-контейнеры уже заняты
|
||
**Проблема:** `outline-postgres-1` и `postgres-zyo10oksv4l1i8s35zhrxijf` — в продакшене (outline, twenty, coolify). Нельзя тащить `atom-db` в них.
|
||
**Решение:** отдельный инстанс `atom-db` с `pgvector`+`pg_trgm`. Проверить свободный порт (сейчас 543x заняты — взять 5434). Тест: `CREATE EXTENSION pgvector;` на пустом БД.
|
||
|
||
### Риск 3: pgvector — не в базовом образе
|
||
**Проблема:** `postgres:16` не включает pgvector по умолчанию.
|
||
**Решение:** образ `pgvector/pgvector:pg16` (официальный, уже с расширением) или `CREATE EXTENSION` при старте контейнера. Проверить: `SELECT name FROM pg_available_extensions WHERE name='pgvector';` → `installed_version` не NULL.
|
||
|
||
### Риск 4: bge-m3 — latency под нагрузкой
|
||
**Проблема:** 1024-мерные эмбеддинги + гибридный поиск на 50 доменах → медленная индексация.
|
||
**Решение:** эмбеддить батчами (по 10-20 атомов), асинхронно, не на критическом пути. Qdrant держит ivfflat/hnw-индекс. Проверить latency: `curl /api/embeddings` → ~200-500ms на запрос (нормально для локальной модели).
|
||
|
||
### Риск 5: Дедуп между техническими и SEO-доменами
|
||
**Проблема:** 50 доменов, часть технические (wiki, docs, сервисы) — их текст «дублируется» с SEO-статьями по терминологии.
|
||
**Решение:** `role='service'` исключается из семантической коллекции Qdrant. Дедуп только между `flagship/satellite/experiment`. Проверить: `WHERE role IN (...)` при эмбеддинге.
|
||
|
||
### Риск 6: pg_trgm + gin — место на диске
|
||
**Проблема:** GIN-индекс по `plain_text` (trigram) на 50 доменах → раздувание.
|
||
**Решение:** только для SEO-доменов, `pg_trgm` на `plain_text` атомов (не на всех полях). Monitor: `SELECT pg_size_pretty(pg_relation_size('atoms'));`.
|
||
|
||
### Риск 7: bge-m3 в Ollama vs embed-сервер
|
||
**Проблема:** Ollama-эмбеддинги — медленнее, чем специализированный embed-сервер (e.g. Instructor, FastEmbed).
|
||
**Решение:** MVP на Ollama bge-m3 (готово). Если latency станет проблемой — вынести embed в отдельный FastAPI-сервис (FastEmbed на CPU, dim=1024, ~10x быстрее). Не делать сразу.
|
||
|
||
### Риск 8: Qdrant-коллекция для атомов
|
||
**Проблема:** Qdrant уже используется (`knowledge_base` и др.) — риск конфликта命名/конфигурации.
|
||
**Решение:** новая коллекция `content_atoms` (dim=1024, distance=Cosine). Не трогать существующие коллекции. Проверить: `curl /collections` → `content_atoms` отсутствует → создать.
|
||
|
||
---
|
||
|
||
*Версия 1.4 — Этапы 1–4 реализованы (atom-db, 3 медиа-канала, AGE-граф). roadmap по этапам + фиксация решений + проактивные риски. Детальные задачи (TDD) — на этапе реализации каждого уровня.*
|