#!/usr/bin/env python3 # Local seeding utilities following 12-factor config from __future__ import annotations import os, sys, hashlib, time from typing import Iterable, Tuple, Callable, List from pathlib import Path # Lazy import psycopg2 only when needed try: import psycopg2 # type: ignore except Exception: # pragma: no cover psycopg2 = None # allow dry-run without dependency DEFAULT_DB = os.getenv("PGDATABASE", os.getenv("DB_NAME", "brain_atomic")) DEFAULT_USER = os.getenv("PGUSER", os.getenv("DB_USER", "postgres")) # Prefer Brain volume sockets if present, then External, then home dir, else localhost DEFAULT_HOST = ( os.getenv("PGHOST") or os.getenv("DB_HOST") or next( ( c for c in [ "/Volumes/Brain/AI_Brain/pg", "/Volumes/Brain/HiveExpansion/pg", "/Volumes/External/AI_Brain/pg", str(Path.home() / "AI_Brain" / "pg"), ] if Path(c).exists() ), "localhost", ) ) DEFAULT_PORT = int(os.getenv("PGPORT", os.getenv("DB_PORT", "5432"))) DRY_RUN = os.getenv("DRY_RUN", "0").lower() in {"1", "true", "yes", "on"} SOURCE_TAG = os.getenv("SEED_SOURCE", "Hive500Expansion") def compute_hash(sentence: str) -> str: # Atomic uniqueness across identical sentences (global), as specified return hashlib.sha256(sentence.encode("utf-8")).hexdigest() def connect_db(): if DRY_RUN: return None if psycopg2 is None: sys.stderr.write( "psycopg2 not installed. Install with: pip install psycopg2-binary\n" ) sys.exit(1) return psycopg2.connect(dbname=DEFAULT_DB, user=DEFAULT_USER, host=DEFAULT_HOST, port=DEFAULT_PORT) INSERT_SQL = ( "INSERT INTO atomic_facts (hash, sentence, category, source, created_at) " "VALUES (%s, %s, %s, %s, now()) ON CONFLICT (hash) DO NOTHING;" ) def insert_many(category: str, items: Iterable[str]) -> Tuple[int, int]: """Insert items; returns (attempted, inserted_estimate).""" attempted = 0 inserted = 0 conn = connect_db() cur = conn.cursor() if conn is not None else None try: for s in items: sentence = s.strip() if not sentence: continue if not sentence.endswith("."): sentence = f"{sentence}." h = compute_hash(sentence) attempted += 1 if DRY_RUN: inserted += 1 continue cur.execute(INSERT_SQL, (h, sentence, category, SOURCE_TAG)) if conn is not None: conn.commit() finally: if cur is not None: cur.close() if conn is not None: conn.close() return attempted, (inserted if DRY_RUN else attempted) def read_file_lines(path: str, limit: int) -> List[str]: seen = set() out: List[str] = [] with open(path, "r", encoding="utf-8") as f: for raw in f: line = raw.strip() if not line: continue if line in seen: continue seen.add(line) out.append(line) if len(out) >= limit: break return out def run_seed(category: str, generator: Callable[[int], Iterable[str]], limit: int = 500) -> None: start = time.time() cat_upper = category.upper() file_override = os.getenv(f"SEED_FILE_{cat_upper}") or os.getenv("SEED_FILE") if file_override and os.path.isfile(file_override): items = read_file_lines(file_override, limit) mode_detail = f"FILE:{file_override}" else: items = list(generator(limit)) mode_detail = "GENERATOR" attempted, inserted = insert_many(category, items) dur = time.time() - start mode = "DRY_RUN" if DRY_RUN else "LIVE" print( f"✅ {mode} seeded category='{category}' source={mode_detail} attempted={attempted} inserted~={inserted} in {dur:.2f}s" )