From 7ca369b6418596d691b2edc5910904af4f0f5a0e Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Thu, 26 Feb 2026 12:59:03 -0500 Subject: [PATCH] feat: add one-time migration script for user notes to profiles Co-Authored-By: Claude Opus 4.6 --- scripts/migrate_notes_to_profiles.py | 78 ++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 scripts/migrate_notes_to_profiles.py diff --git a/scripts/migrate_notes_to_profiles.py b/scripts/migrate_notes_to_profiles.py new file mode 100644 index 0000000..3e940f0 --- /dev/null +++ b/scripts/migrate_notes_to_profiles.py @@ -0,0 +1,78 @@ +"""One-time migration: convert existing timestamped UserNotes into profile summaries. + +Run with: python scripts/migrate_notes_to_profiles.py + +Requires .env with DB_CONNECTION_STRING and LLM env vars. +""" +import asyncio +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) + +from dotenv import load_dotenv +load_dotenv() + +from utils.database import Database +from utils.llm_client import LLMClient + + +async def main(): + db = Database() + if not await db.init(): + print("Database not available.") + return + + llm = LLMClient( + base_url=os.getenv("LLM_BASE_URL", ""), + model=os.getenv("LLM_MODEL", "gpt-4o-mini"), + api_key=os.getenv("LLM_API_KEY", "not-needed"), + ) + + states = await db.load_all_user_states() + migrated = 0 + + for state in states: + notes = state.get("user_notes", "") + if not notes or not notes.strip(): + continue + + # Check if already looks like a profile (no timestamps) + if not any(line.strip().startswith("[") for line in notes.split("\n")): + print(f" User {state['user_id']}: already looks like a profile, skipping.") + continue + + print(f" User {state['user_id']}: migrating notes...") + print(f" Old: {notes[:200]}") + + # Ask LLM to summarize notes into a profile + result = await llm.extract_memories( + conversation=[{"role": "user", "content": f"Here are observation notes about a user:\n{notes}"}], + username="unknown", + current_profile="", + ) + + if result and result.get("profile_update"): + profile = result["profile_update"] + print(f" New: {profile[:200]}") + await db.save_user_state( + user_id=state["user_id"], + offense_count=state["offense_count"], + immune=state["immune"], + off_topic_count=state["off_topic_count"], + baseline_coherence=state.get("baseline_coherence", 0.85), + user_notes=profile, + warned=state.get("warned", False), + last_offense_at=state.get("last_offense_at"), + ) + migrated += 1 + else: + print(f" No profile generated, keeping existing notes.") + + await llm.close() + await db.close() + print(f"\nMigrated {migrated}/{len(states)} user profiles.") + + +if __name__ == "__main__": + asyncio.run(main())