#!/usr/bin/env python3 """ MVP stage 2: compositions — собрать atom_versions под второй канал (Telegram). Для каждого атома генерирует версию под Telegram (plain text, до 1024 символов), если atom_versions для этого канала ещё нет. bge-m3 (dim=1024) — единственная embed-модель (для semantic-версий). Postgres (atom-db) — atom_versions как источник. Запуск: python3 stage2/compositions.py --channel telegram --top 20 python3 stage2/compositions.py --channel telegram --limit 50 """ import argparse import json import os import sys import time import psycopg2 POSTGRES = { "host": os.environ.get("POSTGRES_HOST", "127.0.0.1"), "port": int(os.environ.get("POSTGRES_PORT", "5434")), "user": os.environ.get("POSTGRES_USER", "atom"), "dbname": os.environ.get("POSTGRES_DB", "content_atom"), "password": os.environ.get("POSTGRES_PASSWORD", "atompass123"), } MAX_LEN = 1024 # Telegram limit def get_conn(): return psycopg2.connect(**POSTGRES) def get_atoms(conn, limit: int) -> list[dict]: """Атомы для генерации (draft, не занятые).""" with conn.cursor() as cur: cur.execute(""" SELECT a.id, a.type, a.plain_text, a.word_count, a.project_id FROM atoms a WHERE a.plain_text IS NOT NULL ORDER BY a.created_at DESC LIMIT %s """, (limit,)) cols = [d[0] for d in cur.description] return [dict(zip(cols, row)) for row in cur.fetchall()] def has_version(conn, atom_id: str, channel: str) -> bool: with conn.cursor() as cur: cur.execute( "SELECT 1 FROM atom_versions WHERE atom_id=%s AND channel=%s LIMIT 1", (atom_id, channel), ) return cur.fetchone() is not None def build_telegram_version(atom: dict) -> str: """Генерация версии под Telegram (plain text).""" text = atom["plain_text"] if len(text) > MAX_LEN: text = text[:MAX_LEN].rstrip() + "…" # Типизированное оформление t = atom["type"] if t in ("stat",): return f"📊 {text}" if t in ("qa",): return f"❓ {text}" if t in ("cta",): return f"👉 {text}" if t in ("hook",): return f"🔥 {text}" if t in ("comparison",): return f"⚖️ {text}" if t in ("instruction",): return f"✅ {text}" if t in ("quote",): return f'» {text}' return text def insert_versions(conn, atom_id: str, content: str) -> bool: """Вставить atom_versions под telegram (plain).""" with conn.cursor() as cur: cur.execute(""" INSERT INTO atom_versions (atom_id, channel, format, content, quality_score) VALUES (%s, %s, %s, %s, %s) ON CONFLICT (atom_id, channel, format) DO NOTHING """, (atom_id, "telegram", "plain", content, 7.0)) committed = cur.rowcount > 0 conn.commit() return committed def main(): ap = argparse.ArgumentParser(description="MVP stage 2: compositions (telegram)") ap.add_argument("--channel", default="telegram", help="канал") ap.add_argument("--format", default="plain", help="формат") ap.add_argument("--limit", type=int, default=20, help="сколько атомов") args = ap.parse_args() conn = get_conn() atoms = get_atoms(conn, args.limit) print(f" • {len(atoms)} атомов для {args.channel}") n = 0 start = time.time() for atom in atoms: if has_version(conn, str(atom["id"]), args.channel): continue content = build_telegram_version(atom) if insert_versions(conn, str(atom["id"]), content): n += 1 conn.close() print(f" ✓ atom_versions: {n} за {time.time()-start:.1f}с") print(f"\nMVP stage 2 compositions OK: {n} версий под {args.channel}") return 0 if __name__ == "__main__": sys.exit(main())