Files
content-atom/README.md
T

350 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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-канал)
**Цель:** `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).
### Этап 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 — граф (Memgraph / Apache AGE)
**Цель:** обходы и анализ связей.
- Проекция: Atom → Topic → Entity → Project → Asset.
- graph_outbox — Postgres → воркер → Memgraph (только id + минимальные атрибуты, тело атома НЕ хранится).
- Идемпотентные операции (MERGE по id), ребилд из Postgres за час.
- Сборка через обходы графа (топологическая сортировка).
**KPI:** граф-проекция, сборка через обходы.
### Этап 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.1 — roadmap по этапам + фиксация решений + проактивные риски. Детальные задачи (TDD) — на этапе реализации каждого уровня.*