claude-os/scripts/migrate_to_pgvector.py
2025-10-24 16:17:47 -05:00

418 lines
14 KiB
Python
Executable file

#!/usr/bin/env python3
"""
Migration script to convert from custom schema to PGVectorStore-compatible schema.
This script:
1. Backs up existing data
2. Creates new PGVectorStore-compatible tables
3. Migrates data from old schema to new schema
4. Verifies data integrity
5. Optionally drops old tables
Usage:
python scripts/migrate_to_pgvector.py [--dry-run] [--keep-old]
"""
import sys
import argparse
import psycopg2
from psycopg2.extras import RealDictCursor
from psycopg2 import sql
import json
from datetime import datetime
import uuid
# Connection parameters
import os
DB_CONFIG = {
'host': os.getenv('POSTGRES_HOST', 'host.docker.internal'),
'port': int(os.getenv('POSTGRES_PORT', '5432')),
'database': os.getenv('POSTGRES_DB', 'codeforge'),
'user': os.getenv('POSTGRES_USER', 'iamanmp'),
'password': os.getenv('POSTGRES_PASSWORD', '')
}
class MigrationError(Exception):
"""Custom exception for migration errors"""
pass
def get_connection():
"""Create database connection"""
try:
conn = psycopg2.connect(**DB_CONFIG)
return conn
except Exception as e:
raise MigrationError(f"Failed to connect to database: {e}")
def backup_data(conn):
"""Create backup of existing data"""
print("\n" + "=" * 80)
print("STEP 1: BACKING UP EXISTING DATA")
print("=" * 80)
backup_file = f"backup_codeforge_{datetime.now().strftime('%Y%m%d_%H%M%S')}.sql"
try:
with conn.cursor(cursor_factory=RealDictCursor) as cur:
# Count existing data
cur.execute("SELECT COUNT(*) as count FROM knowledge_bases")
kb_count = cur.fetchone()['count']
cur.execute("SELECT COUNT(*) as count FROM documents")
doc_count = cur.fetchone()['count']
print(f"\n✓ Found {kb_count} knowledge bases")
print(f"✓ Found {doc_count} documents")
# Export to SQL file
import subprocess
result = subprocess.run(
['pg_dump', '-U', DB_CONFIG['user'], '-d', DB_CONFIG['database'],
'-t', 'knowledge_bases', '-t', 'documents', '-f', backup_file],
capture_output=True,
text=True
)
if result.returncode == 0:
print(f"\n✓ Backup created: {backup_file}")
return backup_file
else:
print(f"\n⚠ Warning: Could not create SQL backup: {result.stderr}")
print(" Continuing with in-memory backup...")
return None
except Exception as e:
print(f"\n⚠ Warning: Backup failed: {e}")
print(" Continuing anyway...")
return None
def get_existing_kbs(conn):
"""Get all existing knowledge bases"""
with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute("""
SELECT id, name, kb_type, description, metadata, created_at
FROM knowledge_bases
ORDER BY id
""")
return cur.fetchall()
def sanitize_table_name(kb_name):
"""Convert KB name to valid table name"""
# Convert to lowercase, replace spaces/special chars with underscore
table_name = kb_name.lower()
table_name = ''.join(c if c.isalnum() or c == '_' else '_' for c in table_name)
# Remove consecutive underscores
while '__' in table_name:
table_name = table_name.replace('__', '_')
# Remove leading/trailing underscores
table_name = table_name.strip('_')
# Add data_ prefix
return f"data_{table_name}"
def normalize_kb_types(conn):
"""Normalize KB types to match new schema"""
print("\n" + "=" * 80)
print("STEP 2: NORMALIZING KB TYPES")
print("=" * 80)
type_mapping = {
'documentation': 'DOCUMENTATION',
'code': 'CODE',
'generic': 'GENERIC',
'agent-os': 'AGENT_OS',
'agent_os': 'AGENT_OS'
}
try:
with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute("SELECT id, name, kb_type FROM knowledge_bases")
kbs = cur.fetchall()
for kb in kbs:
old_type = kb['kb_type']
new_type = type_mapping.get(old_type.lower(), 'GENERIC')
if old_type != new_type:
cur.execute("UPDATE knowledge_bases SET kb_type = %s WHERE id = %s",
(new_type, kb['id']))
print(f" {kb['name']}: {old_type} -> {new_type}")
conn.commit()
print("\n✓ KB types normalized")
except Exception as e:
conn.rollback()
raise MigrationError(f"Failed to normalize KB types: {e}")
def create_new_schema(conn):
"""Create new PGVectorStore-compatible schema"""
print("\n" + "=" * 80)
print("STEP 3: CREATING NEW SCHEMA")
print("=" * 80)
try:
with conn.cursor() as cur:
# Read and execute schema file
schema_path = os.path.join(os.path.dirname(__file__), '..', 'app', 'core', 'schema_pgvector.sql')
if not os.path.exists(schema_path):
# Try current directory (for Docker execution)
schema_path = 'schema_pgvector.sql'
with open(schema_path, 'r') as f:
schema_sql = f.read()
cur.execute(schema_sql)
conn.commit()
print("\n✓ New schema created successfully")
except Exception as e:
conn.rollback()
raise MigrationError(f"Failed to create new schema: {e}")
def update_kb_metadata(conn, kb_id, table_name, embed_dim=768):
"""Update knowledge_bases table with new columns"""
with conn.cursor() as cur:
# Add columns if they don't exist
cur.execute("""
ALTER TABLE knowledge_bases
ADD COLUMN IF NOT EXISTS table_name VARCHAR(255),
ADD COLUMN IF NOT EXISTS embed_dim INTEGER DEFAULT 768,
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
""")
# Update the KB record
cur.execute("""
UPDATE knowledge_bases
SET table_name = %s, embed_dim = %s, updated_at = CURRENT_TIMESTAMP
WHERE id = %s
""", (table_name, embed_dim, kb_id))
conn.commit()
def migrate_kb_data(conn, kb, dry_run=False):
"""Migrate data for a single KB"""
kb_id = kb['id']
kb_name = kb['name']
table_name = sanitize_table_name(kb_name)
print(f"\n{'[DRY RUN] ' if dry_run else ''}Migrating KB: {kb_name} -> {table_name}")
try:
with conn.cursor(cursor_factory=RealDictCursor) as cur:
# Get document count
cur.execute("SELECT COUNT(*) as count FROM documents WHERE kb_id = %s", (kb_id,))
doc_count = cur.fetchone()['count']
print(f" Documents to migrate: {doc_count}")
if dry_run:
print(f" [DRY RUN] Would create table: {table_name}")
print(f" [DRY RUN] Would migrate {doc_count} documents")
return True
# Create the new table
cur.execute("SELECT create_kb_table(%s, %s, %s)", (kb_name, table_name, 768))
# Update KB metadata
update_kb_metadata(conn, kb_id, table_name, 768)
# Migrate documents in batches
batch_size = 100
offset = 0
migrated = 0
while True:
cur.execute("""
SELECT doc_id, content, metadata, embedding
FROM documents
WHERE kb_id = %s
ORDER BY id
LIMIT %s OFFSET %s
""", (kb_id, batch_size, offset))
batch = cur.fetchall()
if not batch:
break
# Insert into new table
for doc in batch:
# Generate node_id if doc_id doesn't exist
node_id = doc['doc_id'] if doc['doc_id'] else str(uuid.uuid4())
# Embedding is already in correct format from PostgreSQL
# Just pass it directly
embedding = doc['embedding']
# Insert into new table
insert_query = sql.SQL("""
INSERT INTO {} (text, metadata_, node_id, embedding)
VALUES (%s, %s, %s, %s)
""").format(sql.Identifier(table_name))
cur.execute(insert_query, (
doc['content'],
json.dumps(doc['metadata']) if doc['metadata'] else None,
node_id,
embedding
))
migrated += 1
offset += batch_size
print(f" Migrated: {migrated}/{doc_count}", end='\r')
conn.commit()
print(f" ✓ Migrated: {migrated}/{doc_count}")
# Verify migration
cur.execute(sql.SQL("SELECT COUNT(*) as count FROM {}").format(sql.Identifier(table_name)))
new_count = cur.fetchone()['count']
if new_count != doc_count:
raise MigrationError(f"Verification failed: {new_count} != {doc_count}")
print(f" ✓ Verification passed: {new_count} documents")
return True
except Exception as e:
conn.rollback()
raise MigrationError(f"Failed to migrate KB {kb_name}: {e}")
def verify_migration(conn, kbs):
"""Verify all data was migrated correctly"""
print("\n" + "=" * 80)
print("STEP 5: VERIFYING MIGRATION")
print("=" * 80)
all_good = True
for kb in kbs:
kb_name = kb['name']
table_name = sanitize_table_name(kb_name)
try:
with conn.cursor(cursor_factory=RealDictCursor) as cur:
# Count old documents
cur.execute("SELECT COUNT(*) as count FROM documents WHERE kb_id = %s", (kb['id'],))
old_count = cur.fetchone()['count']
# Count new documents
cur.execute(sql.SQL("SELECT COUNT(*) as count FROM {}").format(sql.Identifier(table_name)))
new_count = cur.fetchone()['count']
if old_count == new_count:
print(f"✓ {kb_name}: {new_count} documents")
else:
print(f"✗ {kb_name}: MISMATCH (old={old_count}, new={new_count})")
all_good = False
except Exception as e:
print(f"✗ {kb_name}: ERROR - {e}")
all_good = False
return all_good
def cleanup_old_schema(conn, dry_run=False):
"""Drop old tables"""
print("\n" + "=" * 80)
print(f"STEP 6: CLEANUP OLD SCHEMA {'[DRY RUN]' if dry_run else ''}")
print("=" * 80)
if dry_run:
print("\n[DRY RUN] Would drop:")
print(" - documents table")
print(" - agent_os_content table (if exists)")
return
try:
with conn.cursor() as cur:
cur.execute("DROP TABLE IF EXISTS documents CASCADE")
print("✓ Dropped documents table")
cur.execute("DROP TABLE IF EXISTS agent_os_content CASCADE")
print("✓ Dropped agent_os_content table")
conn.commit()
except Exception as e:
conn.rollback()
raise MigrationError(f"Failed to cleanup old schema: {e}")
def main():
parser = argparse.ArgumentParser(description='Migrate to PGVectorStore schema')
parser.add_argument('--dry-run', action='store_true', help='Show what would be done without making changes')
parser.add_argument('--keep-old', action='store_true', help='Keep old tables after migration')
args = parser.parse_args()
print("=" * 80)
print("CODE-FORGE: MIGRATION TO PGVECTORSTORE SCHEMA")
print("=" * 80)
if args.dry_run:
print("\n⚠ DRY RUN MODE - No changes will be made\n")
try:
# Connect to database
conn = get_connection()
print("\n✓ Connected to database")
# Backup existing data
if not args.dry_run:
backup_file = backup_data(conn)
# Get existing KBs
kbs = get_existing_kbs(conn)
print(f"\n✓ Found {len(kbs)} knowledge bases to migrate")
# Normalize KB types
if not args.dry_run:
normalize_kb_types(conn)
# Create new schema
if not args.dry_run:
create_new_schema(conn)
# Migrate each KB
print("\n" + "=" * 80)
print("STEP 4: MIGRATING DATA")
print("=" * 80)
for kb in kbs:
migrate_kb_data(conn, kb, dry_run=args.dry_run)
# Verify migration
if not args.dry_run:
if verify_migration(conn, kbs):
print("\n✓ All data verified successfully!")
else:
raise MigrationError("Verification failed!")
# Cleanup old schema
if not args.keep_old:
cleanup_old_schema(conn, dry_run=args.dry_run)
conn.close()
print("\n" + "=" * 80)
print("✓ MIGRATION COMPLETE!")
print("=" * 80)
if not args.dry_run:
print("\nNext steps:")
print("1. Update RAGEngine to use PGVectorStore")
print("2. Update ingestion to use PGVectorStore")
print("3. Test queries on migrated data")
except MigrationError as e:
print(f"\n✗ Migration failed: {e}", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"\n✗ Unexpected error: {e}", file=sys.stderr)
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()