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: панель управляет атомами, юнит-экономика по домену.
Схемы данных
Атомы
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
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
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
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
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
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)
- Хранение атомов: Postgres (истина) + Qdrant (эмбеддинги). Postgres уже в Docker — один чистый инстанс
atom-dbсpgvector+pg_trgm. Атомы/версии/композиции/стоимость — в Postgres. Эмбеддинги + дедупликация — в Qdrant. - Панель: отложить до V5.
- V4 граф: Apache AGE (расширение Postgres), не Memgraph. GraphOutbox/воркер не нужны.
- Эмбеддинги: bge-m3 — единственная модель для MVP и продакшена (установлен в Ollama 2026-09-14, dim=1024, 8192 контекст, мультязычный, гибридный dense+sparse). Не переключаться между nomic/bge-m3 — разный размер вектора (512 vs 1024) и разная система поиска = переиндексация и баги дедупа.
- Домены: 50 доменов, разделение по
role(flagship/satellite/experiment/service). Технические домены (service) НЕ индексируются в семантику — дедуп только между SEO-доменами. Атомы привязаны кproject_id, сборка — в рамках одного домена (защита footprint). - 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) — на этапе реализации каждого уровня.