# 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 — медиа + метрики (видео, подкаст, рассылка) **Цель:** сквозная аналитика от смысла до метрики. - **assets** — медиа-активы (image | audio | video | subtitle), транскрипты. - **atom_assets** — привязка атомов к медиа. - **publications** — публикации (URL/channel_id/external_id) + метрики (views/clicks/conversions/revenue). - **Адаптеры** — видео-сценарий, подкаст, рассылка. **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` (идемпотентно — 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/` → `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.2 — Этап 1 реализован (atom-db + stage1/extract_to_postgres.py). roadmap по этапам + фиксация решений + проактивные риски. Детальные задачи (TDD) — на этапе реализации каждого уровня.*