Add Neo4j schema initialization and validation scripts
- Introduced `neo4j-schema-init.py` for creating the foundational schema for the personal knowledge graph used by multiple AI assistants. - Implemented functionality for creating constraints, indexes, and sample nodes, along with comprehensive testing of the schema. - Added `neo4j-validate.py` to perform validation checks on the Neo4j knowledge graph, including constraints, indexes, sample nodes, relationships, and junk data detection. - Enhanced logging for better traceability and debugging during schema initialization and validation processes.
This commit is contained in:
910
utils/neo4j-schema-init.py
Normal file
910
utils/neo4j-schema-init.py
Normal file
@@ -0,0 +1,910 @@
|
||||
"""
|
||||
Neo4j Unified Knowledge Graph Schema Initialization
|
||||
=====================================================
|
||||
Creates the foundational schema for a unified knowledge graph used by
|
||||
fourteen AI assistants across three teams:
|
||||
|
||||
Personal Team:
|
||||
Hypatia (Learning), Marcus (Fitness), Seneca (Reflection),
|
||||
Nate (Travel), Bowie (Culture), Bourdain (Food),
|
||||
Cousteau (Nature), Garth (Finance), Cristiano (Football)
|
||||
|
||||
Work Team:
|
||||
Alan (Strategy), Ann (Marketing), Jeffrey (Sales), Jarvis (Execution)
|
||||
|
||||
Engineering Team:
|
||||
Scotty (Infrastructure), Harper (Prototyping)
|
||||
|
||||
Schema Reference:
|
||||
docs/neo4j-unified-schema.md
|
||||
|
||||
Requirements:
|
||||
pip install neo4j
|
||||
|
||||
Usage:
|
||||
python neo4j-schema-init.py
|
||||
python neo4j-schema-init.py --uri bolt://ariel.incus:7687
|
||||
python neo4j-schema-init.py --test-only
|
||||
|
||||
Environment Variables (optional):
|
||||
NEO4J_URI - Bolt URI (default: bolt://localhost:7687)
|
||||
NEO4J_USER - Username (default: neo4j)
|
||||
NEO4J_PASSWORD - Password (will prompt if not set)
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import getpass
|
||||
import os
|
||||
import sys
|
||||
from neo4j import GraphDatabase
|
||||
from neo4j.exceptions import AuthError, ServiceUnavailable
|
||||
import logging
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LifeGraphSchema:
|
||||
def __init__(self, uri, user, password):
|
||||
"""Initialize connection to Neo4j database"""
|
||||
self.driver = GraphDatabase.driver(uri, auth=(user, password))
|
||||
self.uri = uri
|
||||
|
||||
def close(self):
|
||||
"""Close the database connection"""
|
||||
self.driver.close()
|
||||
|
||||
def verify_connection(self):
|
||||
"""
|
||||
Verify the connection to Neo4j is working.
|
||||
Returns True if successful, raises exception otherwise.
|
||||
"""
|
||||
with self.driver.session() as session:
|
||||
result = session.run("RETURN 1 AS test")
|
||||
record = result.single()
|
||||
if record and record["test"] == 1:
|
||||
logger.info(f"✓ Connected to Neo4j at {self.uri}")
|
||||
return True
|
||||
raise ConnectionError("Failed to verify Neo4j connection")
|
||||
|
||||
def create_constraints(self):
|
||||
"""
|
||||
Create uniqueness constraints on key node properties.
|
||||
This ensures data integrity and creates indexes automatically.
|
||||
All 74 node types get an id uniqueness constraint.
|
||||
"""
|
||||
constraints = [
|
||||
# ── Universal nodes ──────────────────────────────────────
|
||||
"CREATE CONSTRAINT person_id IF NOT EXISTS FOR (n:Person) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT location_id IF NOT EXISTS FOR (n:Location) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT event_id IF NOT EXISTS FOR (n:Event) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT topic_id IF NOT EXISTS FOR (n:Topic) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT goal_id IF NOT EXISTS FOR (n:Goal) REQUIRE n.id IS UNIQUE",
|
||||
|
||||
# ── Nate: Travel & Adventure ─────────────────────────────
|
||||
"CREATE CONSTRAINT trip_id IF NOT EXISTS FOR (n:Trip) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT destination_id IF NOT EXISTS FOR (n:Destination) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT activity_id IF NOT EXISTS FOR (n:Activity) REQUIRE n.id IS UNIQUE",
|
||||
|
||||
# ── Hypatia: Learning & Reading ──────────────────────────
|
||||
"CREATE CONSTRAINT book_id IF NOT EXISTS FOR (n:Book) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT author_id IF NOT EXISTS FOR (n:Author) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT learningpath_id IF NOT EXISTS FOR (n:LearningPath) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT concept_id IF NOT EXISTS FOR (n:Concept) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT quote_id IF NOT EXISTS FOR (n:Quote) REQUIRE n.id IS UNIQUE",
|
||||
|
||||
# ── Marcus: Fitness & Training ───────────────────────────
|
||||
"CREATE CONSTRAINT training_id IF NOT EXISTS FOR (n:Training) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT exercise_id IF NOT EXISTS FOR (n:Exercise) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT program_id IF NOT EXISTS FOR (n:Program) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT personalrecord_id IF NOT EXISTS FOR (n:PersonalRecord) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT bodymetric_id IF NOT EXISTS FOR (n:BodyMetric) REQUIRE n.id IS UNIQUE",
|
||||
|
||||
# ── Seneca: Reflection & Wellness ────────────────────────
|
||||
"CREATE CONSTRAINT reflection_id IF NOT EXISTS FOR (n:Reflection) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT value_id IF NOT EXISTS FOR (n:Value) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT habit_id IF NOT EXISTS FOR (n:Habit) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT lifeevent_id IF NOT EXISTS FOR (n:LifeEvent) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT intention_id IF NOT EXISTS FOR (n:Intention) REQUIRE n.id IS UNIQUE",
|
||||
|
||||
# ── Bourdain: Food & Cooking ─────────────────────────────
|
||||
"CREATE CONSTRAINT recipe_id IF NOT EXISTS FOR (n:Recipe) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT restaurant_id IF NOT EXISTS FOR (n:Restaurant) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT ingredient_id IF NOT EXISTS FOR (n:Ingredient) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT meal_id IF NOT EXISTS FOR (n:Meal) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT technique_id IF NOT EXISTS FOR (n:Technique) REQUIRE n.id IS UNIQUE",
|
||||
|
||||
# ── Bowie: Arts & Culture ────────────────────────────────
|
||||
"CREATE CONSTRAINT music_id IF NOT EXISTS FOR (n:Music) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT film_id IF NOT EXISTS FOR (n:Film) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT artwork_id IF NOT EXISTS FOR (n:Artwork) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT playlist_id IF NOT EXISTS FOR (n:Playlist) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT artist_id IF NOT EXISTS FOR (n:Artist) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT style_id IF NOT EXISTS FOR (n:Style) REQUIRE n.id IS UNIQUE",
|
||||
|
||||
# ── Cousteau: Nature & Living Things ─────────────────────
|
||||
"CREATE CONSTRAINT species_id IF NOT EXISTS FOR (n:Species) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT plant_id IF NOT EXISTS FOR (n:Plant) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT tank_id IF NOT EXISTS FOR (n:Tank) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT garden_id IF NOT EXISTS FOR (n:Garden) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT ecosystem_id IF NOT EXISTS FOR (n:Ecosystem) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT observation_id IF NOT EXISTS FOR (n:Observation) REQUIRE n.id IS UNIQUE",
|
||||
|
||||
# ── Garth: Personal Finance ──────────────────────────────
|
||||
"CREATE CONSTRAINT account_id IF NOT EXISTS FOR (n:Account) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT investment_id IF NOT EXISTS FOR (n:Investment) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT asset_id IF NOT EXISTS FOR (n:Asset) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT liability_id IF NOT EXISTS FOR (n:Liability) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT budget_id IF NOT EXISTS FOR (n:Budget) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT financialgoal_id IF NOT EXISTS FOR (n:FinancialGoal) REQUIRE n.id IS UNIQUE",
|
||||
|
||||
# ── Cristiano: Football ───────────────────────────────────
|
||||
"CREATE CONSTRAINT match_id IF NOT EXISTS FOR (n:Match) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT team_id IF NOT EXISTS FOR (n:Team) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT league_id IF NOT EXISTS FOR (n:League) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT tournament_id IF NOT EXISTS FOR (n:Tournament) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT player_id IF NOT EXISTS FOR (n:Player) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT season_id IF NOT EXISTS FOR (n:Season) REQUIRE n.id IS UNIQUE",
|
||||
|
||||
# ── Work: Business ───────────────────────────────────────
|
||||
"CREATE CONSTRAINT client_id IF NOT EXISTS FOR (n:Client) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT contact_id IF NOT EXISTS FOR (n:Contact) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT opportunity_id IF NOT EXISTS FOR (n:Opportunity) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT proposal_id IF NOT EXISTS FOR (n:Proposal) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT project_id IF NOT EXISTS FOR (n:Project) REQUIRE n.id IS UNIQUE",
|
||||
|
||||
# ── Work: Market Intelligence ────────────────────────────
|
||||
"CREATE CONSTRAINT vendor_id IF NOT EXISTS FOR (n:Vendor) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT competitor_id IF NOT EXISTS FOR (n:Competitor) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT markettrend_id IF NOT EXISTS FOR (n:MarketTrend) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT technology_id IF NOT EXISTS FOR (n:Technology) REQUIRE n.id IS UNIQUE",
|
||||
|
||||
# ── Work: Content & Visibility ───────────────────────────
|
||||
"CREATE CONSTRAINT content_id IF NOT EXISTS FOR (n:Content) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT publication_id IF NOT EXISTS FOR (n:Publication) REQUIRE n.id IS UNIQUE",
|
||||
|
||||
# ── Work: Professional Development ───────────────────────
|
||||
"CREATE CONSTRAINT skill_id IF NOT EXISTS FOR (n:Skill) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT certification_id IF NOT EXISTS FOR (n:Certification) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT relationship_id IF NOT EXISTS FOR (n:Relationship) REQUIRE n.id IS UNIQUE",
|
||||
|
||||
# ── Work: Daily Operations ───────────────────────────────
|
||||
"CREATE CONSTRAINT task_id IF NOT EXISTS FOR (n:Task) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT meeting_id IF NOT EXISTS FOR (n:Meeting) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT note_id IF NOT EXISTS FOR (n:Note) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT decision_id IF NOT EXISTS FOR (n:Decision) REQUIRE n.id IS UNIQUE",
|
||||
|
||||
# ── Engineering: Scotty ──────────────────────────────────
|
||||
"CREATE CONSTRAINT infrastructure_id IF NOT EXISTS FOR (n:Infrastructure) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT incident_id IF NOT EXISTS FOR (n:Incident) REQUIRE n.id IS UNIQUE",
|
||||
|
||||
# ── Engineering: Harper ──────────────────────────────────
|
||||
"CREATE CONSTRAINT prototype_id IF NOT EXISTS FOR (n:Prototype) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT experiment_id IF NOT EXISTS FOR (n:Experiment) REQUIRE n.id IS UNIQUE",
|
||||
]
|
||||
|
||||
with self.driver.session() as session:
|
||||
created = 0
|
||||
for constraint in constraints:
|
||||
try:
|
||||
session.run(constraint)
|
||||
name = constraint.split("CONSTRAINT")[1].split("IF")[0].strip()
|
||||
logger.info(f" ✓ Constraint: {name}")
|
||||
created += 1
|
||||
except Exception as e:
|
||||
logger.warning(f" ⚠ Constraint may already exist: {e}")
|
||||
logger.info(f"Constraints processed: {created}/{len(constraints)}")
|
||||
|
||||
def create_indexes(self):
|
||||
"""
|
||||
Create indexes for frequently queried properties.
|
||||
These improve query performance for searches and filters.
|
||||
Organized by query pattern: name/title, date, type/status, domain.
|
||||
"""
|
||||
indexes = [
|
||||
# ── Name / Title text search ─────────────────────────────
|
||||
"CREATE INDEX person_name IF NOT EXISTS FOR (n:Person) ON (n.name)",
|
||||
"CREATE INDEX location_name IF NOT EXISTS FOR (n:Location) ON (n.name)",
|
||||
"CREATE INDEX topic_name IF NOT EXISTS FOR (n:Topic) ON (n.name)",
|
||||
"CREATE INDEX goal_name IF NOT EXISTS FOR (n:Goal) ON (n.name)",
|
||||
"CREATE INDEX book_title IF NOT EXISTS FOR (n:Book) ON (n.title)",
|
||||
"CREATE INDEX film_title IF NOT EXISTS FOR (n:Film) ON (n.title)",
|
||||
"CREATE INDEX music_title IF NOT EXISTS FOR (n:Music) ON (n.title)",
|
||||
"CREATE INDEX artwork_title IF NOT EXISTS FOR (n:Artwork) ON (n.title)",
|
||||
"CREATE INDEX recipe_name IF NOT EXISTS FOR (n:Recipe) ON (n.name)",
|
||||
"CREATE INDEX restaurant_name IF NOT EXISTS FOR (n:Restaurant) ON (n.name)",
|
||||
"CREATE INDEX exercise_name IF NOT EXISTS FOR (n:Exercise) ON (n.name)",
|
||||
"CREATE INDEX species_name IF NOT EXISTS FOR (n:Species) ON (n.name)",
|
||||
"CREATE INDEX plant_name IF NOT EXISTS FOR (n:Plant) ON (n.name)",
|
||||
"CREATE INDEX ingredient_name IF NOT EXISTS FOR (n:Ingredient) ON (n.name)",
|
||||
"CREATE INDEX artist_name IF NOT EXISTS FOR (n:Artist) ON (n.name)",
|
||||
"CREATE INDEX author_name IF NOT EXISTS FOR (n:Author) ON (n.name)",
|
||||
"CREATE INDEX concept_name IF NOT EXISTS FOR (n:Concept) ON (n.name)",
|
||||
"CREATE INDEX client_name IF NOT EXISTS FOR (n:Client) ON (n.name)",
|
||||
"CREATE INDEX contact_name IF NOT EXISTS FOR (n:Contact) ON (n.name)",
|
||||
"CREATE INDEX vendor_name IF NOT EXISTS FOR (n:Vendor) ON (n.name)",
|
||||
"CREATE INDEX competitor_name IF NOT EXISTS FOR (n:Competitor) ON (n.name)",
|
||||
"CREATE INDEX technology_name IF NOT EXISTS FOR (n:Technology) ON (n.name)",
|
||||
"CREATE INDEX content_title IF NOT EXISTS FOR (n:Content) ON (n.title)",
|
||||
"CREATE INDEX skill_name IF NOT EXISTS FOR (n:Skill) ON (n.name)",
|
||||
"CREATE INDEX task_title IF NOT EXISTS FOR (n:Task) ON (n.title)",
|
||||
"CREATE INDEX meeting_title IF NOT EXISTS FOR (n:Meeting) ON (n.title)",
|
||||
"CREATE INDEX infrastructure_name IF NOT EXISTS FOR (n:Infrastructure) ON (n.name)",
|
||||
"CREATE INDEX prototype_name IF NOT EXISTS FOR (n:Prototype) ON (n.name)",
|
||||
"CREATE INDEX investment_ticker IF NOT EXISTS FOR (n:Investment) ON (n.ticker)",
|
||||
"CREATE INDEX match_home IF NOT EXISTS FOR (n:Match) ON (n.home_team)",
|
||||
"CREATE INDEX match_away IF NOT EXISTS FOR (n:Match) ON (n.away_team)",
|
||||
"CREATE INDEX team_name IF NOT EXISTS FOR (n:Team) ON (n.name)",
|
||||
"CREATE INDEX league_name IF NOT EXISTS FOR (n:League) ON (n.name)",
|
||||
"CREATE INDEX tournament_name IF NOT EXISTS FOR (n:Tournament) ON (n.name)",
|
||||
"CREATE INDEX player_name IF NOT EXISTS FOR (n:Player) ON (n.name)",
|
||||
"CREATE INDEX season_team IF NOT EXISTS FOR (n:Season) ON (n.team)",
|
||||
|
||||
# ── Date indexes for temporal queries ────────────────────
|
||||
"CREATE INDEX event_date IF NOT EXISTS FOR (n:Event) ON (n.date)",
|
||||
"CREATE INDEX training_date IF NOT EXISTS FOR (n:Training) ON (n.date)",
|
||||
"CREATE INDEX trip_start IF NOT EXISTS FOR (n:Trip) ON (n.start_date)",
|
||||
"CREATE INDEX reflection_date IF NOT EXISTS FOR (n:Reflection) ON (n.date)",
|
||||
"CREATE INDEX observation_date IF NOT EXISTS FOR (n:Observation) ON (n.date)",
|
||||
"CREATE INDEX meal_date IF NOT EXISTS FOR (n:Meal) ON (n.date)",
|
||||
"CREATE INDEX meeting_date IF NOT EXISTS FOR (n:Meeting) ON (n.date)",
|
||||
"CREATE INDEX task_due IF NOT EXISTS FOR (n:Task) ON (n.due_date)",
|
||||
"CREATE INDEX note_date IF NOT EXISTS FOR (n:Note) ON (n.date)",
|
||||
"CREATE INDEX decision_date IF NOT EXISTS FOR (n:Decision) ON (n.date)",
|
||||
"CREATE INDEX incident_date IF NOT EXISTS FOR (n:Incident) ON (n.date)",
|
||||
"CREATE INDEX bodymetric_date IF NOT EXISTS FOR (n:BodyMetric) ON (n.date)",
|
||||
"CREATE INDEX personalrecord_date IF NOT EXISTS FOR (n:PersonalRecord) ON (n.date)",
|
||||
"CREATE INDEX lifeevent_date IF NOT EXISTS FOR (n:LifeEvent) ON (n.date)",
|
||||
"CREATE INDEX intention_date IF NOT EXISTS FOR (n:Intention) ON (n.date)",
|
||||
"CREATE INDEX match_date IF NOT EXISTS FOR (n:Match) ON (n.date)",
|
||||
|
||||
# ── Type / Status / Category indexes ─────────────────────
|
||||
"CREATE INDEX event_type IF NOT EXISTS FOR (n:Event) ON (n.type)",
|
||||
"CREATE INDEX location_type IF NOT EXISTS FOR (n:Location) ON (n.type)",
|
||||
"CREATE INDEX activity_type IF NOT EXISTS FOR (n:Activity) ON (n.type)",
|
||||
"CREATE INDEX training_type IF NOT EXISTS FOR (n:Training) ON (n.type)",
|
||||
"CREATE INDEX music_genre IF NOT EXISTS FOR (n:Music) ON (n.genre)",
|
||||
"CREATE INDEX species_category IF NOT EXISTS FOR (n:Species) ON (n.category)",
|
||||
"CREATE INDEX exercise_category IF NOT EXISTS FOR (n:Exercise) ON (n.category)",
|
||||
"CREATE INDEX book_status IF NOT EXISTS FOR (n:Book) ON (n.status)",
|
||||
"CREATE INDEX trip_status IF NOT EXISTS FOR (n:Trip) ON (n.status)",
|
||||
"CREATE INDEX goal_status IF NOT EXISTS FOR (n:Goal) ON (n.status)",
|
||||
"CREATE INDEX goal_category IF NOT EXISTS FOR (n:Goal) ON (n.category)",
|
||||
"CREATE INDEX habit_status IF NOT EXISTS FOR (n:Habit) ON (n.status)",
|
||||
"CREATE INDEX program_status IF NOT EXISTS FOR (n:Program) ON (n.status)",
|
||||
"CREATE INDEX client_status IF NOT EXISTS FOR (n:Client) ON (n.status)",
|
||||
"CREATE INDEX opportunity_status IF NOT EXISTS FOR (n:Opportunity) ON (n.status)",
|
||||
"CREATE INDEX proposal_status IF NOT EXISTS FOR (n:Proposal) ON (n.status)",
|
||||
"CREATE INDEX project_status IF NOT EXISTS FOR (n:Project) ON (n.status)",
|
||||
"CREATE INDEX task_status IF NOT EXISTS FOR (n:Task) ON (n.status)",
|
||||
"CREATE INDEX task_priority IF NOT EXISTS FOR (n:Task) ON (n.priority)",
|
||||
"CREATE INDEX content_status IF NOT EXISTS FOR (n:Content) ON (n.status)",
|
||||
"CREATE INDEX content_type IF NOT EXISTS FOR (n:Content) ON (n.type)",
|
||||
"CREATE INDEX incident_severity IF NOT EXISTS FOR (n:Incident) ON (n.severity)",
|
||||
"CREATE INDEX incident_status IF NOT EXISTS FOR (n:Incident) ON (n.status)",
|
||||
"CREATE INDEX infrastructure_status IF NOT EXISTS FOR (n:Infrastructure) ON (n.status)",
|
||||
"CREATE INDEX account_type IF NOT EXISTS FOR (n:Account) ON (n.type)",
|
||||
"CREATE INDEX investment_type IF NOT EXISTS FOR (n:Investment) ON (n.type)",
|
||||
"CREATE INDEX liability_type IF NOT EXISTS FOR (n:Liability) ON (n.type)",
|
||||
"CREATE INDEX financialgoal_status IF NOT EXISTS FOR (n:FinancialGoal) ON (n.status)",
|
||||
"CREATE INDEX skill_category IF NOT EXISTS FOR (n:Skill) ON (n.category)",
|
||||
"CREATE INDEX skill_level IF NOT EXISTS FOR (n:Skill) ON (n.level)",
|
||||
"CREATE INDEX vendor_category IF NOT EXISTS FOR (n:Vendor) ON (n.category)",
|
||||
"CREATE INDEX match_competition IF NOT EXISTS FOR (n:Match) ON (n.competition)",
|
||||
"CREATE INDEX team_league IF NOT EXISTS FOR (n:Team) ON (n.league)",
|
||||
"CREATE INDEX player_position IF NOT EXISTS FOR (n:Player) ON (n.position)",
|
||||
"CREATE INDEX player_team IF NOT EXISTS FOR (n:Player) ON (n.team)",
|
||||
"CREATE INDEX league_country IF NOT EXISTS FOR (n:League) ON (n.country)",
|
||||
"CREATE INDEX season_year IF NOT EXISTS FOR (n:Season) ON (n.season_year)",
|
||||
|
||||
# ── Domain indexes for cross-team filtering ──────────────
|
||||
"CREATE INDEX event_domain IF NOT EXISTS FOR (n:Event) ON (n.domain)",
|
||||
"CREATE INDEX topic_domain IF NOT EXISTS FOR (n:Topic) ON (n.domain)",
|
||||
"CREATE INDEX goal_domain IF NOT EXISTS FOR (n:Goal) ON (n.domain)",
|
||||
"CREATE INDEX location_domain IF NOT EXISTS FOR (n:Location) ON (n.domain)",
|
||||
"CREATE INDEX person_domain IF NOT EXISTS FOR (n:Person) ON (n.domain)",
|
||||
]
|
||||
|
||||
with self.driver.session() as session:
|
||||
created = 0
|
||||
for index in indexes:
|
||||
try:
|
||||
session.run(index)
|
||||
name = index.split("INDEX")[1].split("IF")[0].strip()
|
||||
logger.info(f" ✓ Index: {name}")
|
||||
created += 1
|
||||
except Exception as e:
|
||||
logger.warning(f" ⚠ Index may already exist: {e}")
|
||||
logger.info(f"Indexes processed: {created}/{len(indexes)}")
|
||||
|
||||
def verify_schema(self):
|
||||
"""
|
||||
Verify that constraints and indexes were created successfully.
|
||||
Returns a dict with counts and status.
|
||||
"""
|
||||
results = {"constraints": 0, "indexes": 0, "nodes": 0, "success": True}
|
||||
|
||||
with self.driver.session() as session:
|
||||
# Count constraints
|
||||
constraint_result = session.run("SHOW CONSTRAINTS")
|
||||
constraints = list(constraint_result)
|
||||
results["constraints"] = len(constraints)
|
||||
|
||||
# Count indexes (excluding constraint-created ones)
|
||||
index_result = session.run("SHOW INDEXES WHERE type = 'RANGE'")
|
||||
indexes = list(index_result)
|
||||
results["indexes"] = len(indexes)
|
||||
|
||||
# Count nodes
|
||||
node_result = session.run("MATCH (n) RETURN count(n) AS count")
|
||||
results["nodes"] = node_result.single()["count"]
|
||||
|
||||
return results
|
||||
|
||||
def run_tests(self, include_schema_tests=True):
|
||||
"""
|
||||
Run comprehensive tests to verify schema and APOC functionality.
|
||||
Returns True if all tests pass, False otherwise.
|
||||
|
||||
Args:
|
||||
include_schema_tests: If True, also verify constraints/indexes exist
|
||||
"""
|
||||
tests_passed = 0
|
||||
tests_failed = 0
|
||||
|
||||
test_cases = [
|
||||
("Connection test", "RETURN 1 AS result", lambda r: r.single()["result"] == 1),
|
||||
("APOC available", "RETURN apoc.version() AS version", lambda r: r.single()["version"] is not None),
|
||||
("Create test node",
|
||||
"CREATE (t:_Test {id: 'test_' + toString(timestamp())}) RETURN t.id AS id",
|
||||
lambda r: r.single()["id"] is not None),
|
||||
("Query test node",
|
||||
"MATCH (t:_Test) RETURN count(t) AS count",
|
||||
lambda r: r.single()["count"] >= 1),
|
||||
("APOC collection functions",
|
||||
"RETURN apoc.coll.sum([1,2,3]) AS total",
|
||||
lambda r: r.single()["total"] == 6),
|
||||
("APOC date functions",
|
||||
"RETURN apoc.date.format(timestamp(), 'ms', 'yyyy-MM-dd') AS today",
|
||||
lambda r: len(r.single()["today"]) == 10),
|
||||
]
|
||||
|
||||
# Schema-specific tests
|
||||
schema_tests = [
|
||||
# Universal nodes
|
||||
("Constraint: Person",
|
||||
"SHOW CONSTRAINTS WHERE name = 'person_id'",
|
||||
lambda r: len(list(r)) == 1),
|
||||
("Constraint: Location",
|
||||
"SHOW CONSTRAINTS WHERE name = 'location_id'",
|
||||
lambda r: len(list(r)) == 1),
|
||||
("Constraint: Topic",
|
||||
"SHOW CONSTRAINTS WHERE name = 'topic_id'",
|
||||
lambda r: len(list(r)) == 1),
|
||||
("Constraint: Goal",
|
||||
"SHOW CONSTRAINTS WHERE name = 'goal_id'",
|
||||
lambda r: len(list(r)) == 1),
|
||||
# Personal team samples
|
||||
("Constraint: Book",
|
||||
"SHOW CONSTRAINTS WHERE name = 'book_id'",
|
||||
lambda r: len(list(r)) == 1),
|
||||
("Constraint: Training",
|
||||
"SHOW CONSTRAINTS WHERE name = 'training_id'",
|
||||
lambda r: len(list(r)) == 1),
|
||||
("Constraint: Recipe",
|
||||
"SHOW CONSTRAINTS WHERE name = 'recipe_id'",
|
||||
lambda r: len(list(r)) == 1),
|
||||
("Constraint: Account",
|
||||
"SHOW CONSTRAINTS WHERE name = 'account_id'",
|
||||
lambda r: len(list(r)) == 1),
|
||||
# Work team samples
|
||||
("Constraint: Client",
|
||||
"SHOW CONSTRAINTS WHERE name = 'client_id'",
|
||||
lambda r: len(list(r)) == 1),
|
||||
("Constraint: Opportunity",
|
||||
"SHOW CONSTRAINTS WHERE name = 'opportunity_id'",
|
||||
lambda r: len(list(r)) == 1),
|
||||
("Constraint: Task",
|
||||
"SHOW CONSTRAINTS WHERE name = 'task_id'",
|
||||
lambda r: len(list(r)) == 1),
|
||||
# Engineering team samples
|
||||
("Constraint: Infrastructure",
|
||||
"SHOW CONSTRAINTS WHERE name = 'infrastructure_id'",
|
||||
lambda r: len(list(r)) == 1),
|
||||
("Constraint: Prototype",
|
||||
"SHOW CONSTRAINTS WHERE name = 'prototype_id'",
|
||||
lambda r: len(list(r)) == 1),
|
||||
# Index checks
|
||||
("Index: person_name",
|
||||
"SHOW INDEXES WHERE name = 'person_name'",
|
||||
lambda r: len(list(r)) == 1),
|
||||
("Index: event_domain",
|
||||
"SHOW INDEXES WHERE name = 'event_domain'",
|
||||
lambda r: len(list(r)) == 1),
|
||||
("Index: client_status",
|
||||
"SHOW INDEXES WHERE name = 'client_status'",
|
||||
lambda r: len(list(r)) == 1),
|
||||
# Cristiano team sample
|
||||
("Constraint: Match",
|
||||
"SHOW CONSTRAINTS WHERE name = 'match_id'",
|
||||
lambda r: len(list(r)) == 1),
|
||||
("Constraint: Team",
|
||||
"SHOW CONSTRAINTS WHERE name = 'team_id'",
|
||||
lambda r: len(list(r)) == 1),
|
||||
# Total constraint count (74 node types)
|
||||
("Total constraints >= 74",
|
||||
"SHOW CONSTRAINTS",
|
||||
lambda r: len(list(r)) >= 74),
|
||||
]
|
||||
|
||||
if include_schema_tests:
|
||||
test_cases.extend(schema_tests)
|
||||
|
||||
logger.info("\n" + "=" * 60)
|
||||
logger.info("RUNNING SCHEMA VERIFICATION TESTS")
|
||||
logger.info("=" * 60)
|
||||
|
||||
with self.driver.session() as session:
|
||||
for test_name, query, validator in test_cases:
|
||||
try:
|
||||
result = session.run(query)
|
||||
if validator(result):
|
||||
logger.info(f" ✓ {test_name}")
|
||||
tests_passed += 1
|
||||
else:
|
||||
logger.error(f" ✗ {test_name} - Validation failed")
|
||||
tests_failed += 1
|
||||
except Exception as e:
|
||||
logger.error(f" ✗ {test_name} - {e}")
|
||||
tests_failed += 1
|
||||
|
||||
# Cleanup test nodes
|
||||
try:
|
||||
session.run("MATCH (t:_Test) DELETE t")
|
||||
logger.info(" ✓ Cleanup test nodes")
|
||||
except Exception as e:
|
||||
logger.warning(f" ⚠ Cleanup failed: {e}")
|
||||
|
||||
logger.info("=" * 60)
|
||||
logger.info(f"Tests: {tests_passed} passed, {tests_failed} failed")
|
||||
logger.info("=" * 60 + "\n")
|
||||
|
||||
return tests_failed == 0
|
||||
|
||||
def create_sample_nodes(self):
|
||||
"""
|
||||
Create sample nodes spanning all three teams to demonstrate
|
||||
the unified schema and cross-domain relationships.
|
||||
Uses explicit write transactions for reliable commits.
|
||||
"""
|
||||
node_queries = [
|
||||
# ── Central person node ──────────────────────────────────
|
||||
("Person:user_main", """
|
||||
MERGE (p:Person {id: 'user_main'})
|
||||
ON CREATE SET p.created_at = datetime()
|
||||
SET p.name = 'Main User',
|
||||
p.relationship = 'self',
|
||||
p.domain = 'both',
|
||||
p.updated_at = datetime()
|
||||
RETURN p.id AS id
|
||||
"""),
|
||||
|
||||
# ── Personal: Sample location ────────────────────────────
|
||||
("Location:location_home", """
|
||||
MERGE (l:Location {id: 'location_home'})
|
||||
ON CREATE SET l.created_at = datetime()
|
||||
SET l.name = 'Home',
|
||||
l.type = 'residence',
|
||||
l.domain = 'personal',
|
||||
l.updated_at = datetime()
|
||||
RETURN l.id AS id
|
||||
"""),
|
||||
|
||||
# ── Personal: Sample trip (Nate) ─────────────────────────
|
||||
("Trip:trip_sample_2025", """
|
||||
MERGE (t:Trip {id: 'trip_sample_2025'})
|
||||
ON CREATE SET t.created_at = datetime()
|
||||
SET t.name = 'Sample Trip',
|
||||
t.status = 'planning',
|
||||
t.updated_at = datetime()
|
||||
RETURN t.id AS id
|
||||
"""),
|
||||
|
||||
# ── Personal: Sample book (Hypatia) ──────────────────────
|
||||
("Book:book_meditations_aurelius", """
|
||||
MERGE (b:Book {id: 'book_meditations_aurelius'})
|
||||
ON CREATE SET b.created_at = datetime()
|
||||
SET b.title = 'Meditations',
|
||||
b.author = 'Marcus Aurelius',
|
||||
b.status = 'completed',
|
||||
b.rating = 5,
|
||||
b.updated_at = datetime()
|
||||
RETURN b.id AS id
|
||||
"""),
|
||||
|
||||
# ── Personal: Sample goal (Seneca) ───────────────────────
|
||||
("Goal:goal_sample_2025", """
|
||||
MERGE (g:Goal {id: 'goal_sample_2025'})
|
||||
ON CREATE SET g.created_at = datetime()
|
||||
SET g.name = 'Sample Goal',
|
||||
g.category = 'personal_growth',
|
||||
g.domain = 'personal',
|
||||
g.status = 'in_progress',
|
||||
g.updated_at = datetime()
|
||||
RETURN g.id AS id
|
||||
"""),
|
||||
|
||||
# ── Personal: Sample topic (universal) ───────────────────
|
||||
("Topic:topic_stoicism", """
|
||||
MERGE (t:Topic {id: 'topic_stoicism'})
|
||||
ON CREATE SET t.created_at = datetime()
|
||||
SET t.name = 'Stoicism',
|
||||
t.category = 'philosophy',
|
||||
t.domain = 'personal',
|
||||
t.updated_at = datetime()
|
||||
RETURN t.id AS id
|
||||
"""),
|
||||
|
||||
# ── Personal: Sample account (Garth) ─────────────────────
|
||||
("Account:account_tfsa_sample", """
|
||||
MERGE (a:Account {id: 'account_tfsa_sample'})
|
||||
ON CREATE SET a.created_at = datetime()
|
||||
SET a.name = 'TFSA - Sample',
|
||||
a.type = 'TFSA',
|
||||
a.updated_at = datetime()
|
||||
RETURN a.id AS id
|
||||
"""),
|
||||
|
||||
# ── Work: Sample client ──────────────────────────────────
|
||||
("Client:client_sample_corp", """
|
||||
MERGE (c:Client {id: 'client_sample_corp'})
|
||||
ON CREATE SET c.created_at = datetime()
|
||||
SET c.name = 'Sample Corp',
|
||||
c.industry = 'Technology',
|
||||
c.status = 'prospect',
|
||||
c.updated_at = datetime()
|
||||
RETURN c.id AS id
|
||||
"""),
|
||||
|
||||
# ── Work: Sample skill ───────────────────────────────────
|
||||
("Skill:skill_cx_strategy", """
|
||||
MERGE (s:Skill {id: 'skill_cx_strategy'})
|
||||
ON CREATE SET s.created_at = datetime()
|
||||
SET s.name = 'CX Strategy',
|
||||
s.category = 'consulting',
|
||||
s.level = 'expert',
|
||||
s.updated_at = datetime()
|
||||
RETURN s.id AS id
|
||||
"""),
|
||||
|
||||
# ── Work: Sample topic ───────────────────────────────────
|
||||
("Topic:topic_ai_in_cx", """
|
||||
MERGE (t:Topic {id: 'topic_ai_in_cx'})
|
||||
ON CREATE SET t.created_at = datetime()
|
||||
SET t.name = 'AI in Customer Experience',
|
||||
t.category = 'technology',
|
||||
t.domain = 'work',
|
||||
t.updated_at = datetime()
|
||||
RETURN t.id AS id
|
||||
"""),
|
||||
|
||||
# ── Engineering: Sample infrastructure (Scotty) ──────────
|
||||
("Infrastructure:infra_neo4j_prod", """
|
||||
MERGE (i:Infrastructure {id: 'infra_neo4j_prod'})
|
||||
ON CREATE SET i.created_at = datetime()
|
||||
SET i.name = 'Neo4j Production',
|
||||
i.type = 'database',
|
||||
i.status = 'running',
|
||||
i.environment = 'production',
|
||||
i.updated_at = datetime()
|
||||
RETURN i.id AS id
|
||||
"""),
|
||||
|
||||
# ── Personal: Sample team (Cristiano) ──────────────────────
|
||||
("Team:team_arsenal", """
|
||||
MERGE (t:Team {id: 'team_arsenal'})
|
||||
ON CREATE SET t.created_at = datetime()
|
||||
SET t.name = 'Arsenal',
|
||||
t.league = 'Premier League',
|
||||
t.country = 'England',
|
||||
t.followed = true,
|
||||
t.updated_at = datetime()
|
||||
RETURN t.id AS id
|
||||
"""),
|
||||
]
|
||||
|
||||
# Create all nodes in one explicit transaction (auto-commits on exit)
|
||||
created_nodes = 0
|
||||
with self.driver.session() as session:
|
||||
with session.begin_transaction() as tx:
|
||||
for label, query in node_queries:
|
||||
try:
|
||||
result = tx.run(query)
|
||||
record = result.single()
|
||||
logger.info(f" ✓ Node: {label} → {record['id']}")
|
||||
created_nodes += 1
|
||||
except Exception as e:
|
||||
logger.error(f" ✗ Node {label}: {e}")
|
||||
# tx auto-commits when context exits normally
|
||||
|
||||
logger.info(f" Created {created_nodes}/{len(node_queries)} sample nodes")
|
||||
|
||||
# Verify nodes exist before creating relationships
|
||||
with self.driver.session() as session:
|
||||
count = session.run("MATCH (n) RETURN count(n) AS c").single()["c"]
|
||||
logger.info(f" Verified {count} nodes exist before creating relationships")
|
||||
|
||||
# Create all relationships in one explicit transaction
|
||||
rel_specs = [
|
||||
("SUPPORTS", "Person", "user_main", "Team", "team_arsenal"),
|
||||
("COMPLETED", "Person", "user_main", "Book", "book_meditations_aurelius"),
|
||||
("PURSUING", "Person", "user_main", "Goal", "goal_sample_2025"),
|
||||
("EXPLORES", "Book", "book_meditations_aurelius", "Topic", "topic_stoicism"),
|
||||
("OWNS", "Person", "user_main", "Account", "account_tfsa_sample"),
|
||||
]
|
||||
|
||||
created_rels = 0
|
||||
with self.driver.session() as session:
|
||||
with session.begin_transaction() as tx:
|
||||
for rel_type, from_label, from_id, to_label, to_id in rel_specs:
|
||||
desc = f"({from_id})-[:{rel_type}]->({to_id})"
|
||||
try:
|
||||
query = (
|
||||
f"MATCH (a:{from_label} {{id: $from_id}}) "
|
||||
f"MATCH (b:{to_label} {{id: $to_id}}) "
|
||||
f"MERGE (a)-[r:{rel_type}]->(b) "
|
||||
f"RETURN type(r) AS rel"
|
||||
)
|
||||
result = tx.run(query, from_id=from_id, to_id=to_id)
|
||||
record = result.single()
|
||||
if record is None:
|
||||
logger.error(f" ✗ Rel {desc}: endpoints not found")
|
||||
else:
|
||||
logger.info(f" ✓ Rel: {desc}")
|
||||
created_rels += 1
|
||||
except Exception as e:
|
||||
logger.error(f" ✗ Rel {desc}: {e}")
|
||||
# tx auto-commits when context exits normally
|
||||
|
||||
logger.info(f" Created {created_rels}/{len(rel_specs)} sample relationships")
|
||||
|
||||
def document_schema(self):
|
||||
"""
|
||||
Display a summary of the unified schema design.
|
||||
Full documentation: docs/neo4j-unified-schema.md
|
||||
"""
|
||||
schema_doc = """
|
||||
════════════════════════════════════════════════════════════════
|
||||
UNIFIED KNOWLEDGE GRAPH SCHEMA
|
||||
One graph for all assistants across personal, work, and engineering
|
||||
════════════════════════════════════════════════════════════════
|
||||
|
||||
UNIVERSAL NODES (any assistant can read/write):
|
||||
────────────────────────────────────────────────────────────────
|
||||
Person People (self, family, friends, colleagues)
|
||||
Location Physical places (cities, venues, offices, trails)
|
||||
Event Significant occurrences (celebrations, conferences)
|
||||
Topic Subjects of interest (stoicism, AI in CX)
|
||||
Goal Objectives (personal growth, career, fitness, financial)
|
||||
|
||||
PERSONAL TEAM:
|
||||
────────────────────────────────────────────────────────────────
|
||||
Nate (Travel) Trip, Destination, Activity
|
||||
Hypatia (Learning) Book, Author, LearningPath, Concept, Quote
|
||||
Marcus (Fitness) Training, Exercise, Program, PersonalRecord, BodyMetric
|
||||
Seneca (Reflection) Reflection, Value, Habit, LifeEvent, Intention
|
||||
Bourdain (Food) Recipe, Restaurant, Ingredient, Meal, Technique
|
||||
Bowie (Culture) Music, Film, Artwork, Playlist, Artist, Style
|
||||
Cousteau (Nature) Species, Plant, Tank, Garden, Ecosystem, Observation
|
||||
Garth (Finance) Account, Investment, Asset, Liability, Budget, FinancialGoal
|
||||
Cristiano (Football) Match, Team, League, Tournament, Player, Season
|
||||
|
||||
WORK TEAM:
|
||||
────────────────────────────────────────────────────────────────
|
||||
Alan (Strategy) Client, Vendor, Competitor, MarketTrend, Technology, Decision
|
||||
Ann (Marketing) Content, Publication, Topic, Event
|
||||
Jeffrey (Sales) Contact, Opportunity, Proposal, Meeting
|
||||
Jarvis (Execution) Task, Meeting, Note, Decision, Project
|
||||
|
||||
ENGINEERING TEAM:
|
||||
────────────────────────────────────────────────────────────────
|
||||
Scotty (Infra) Infrastructure, Incident
|
||||
Harper (Hacking) Prototype, Experiment
|
||||
|
||||
TOTAL: 74 node types, all with id uniqueness constraints
|
||||
|
||||
CROSS-TEAM CONNECTIONS (examples):
|
||||
────────────────────────────────────────────────────────────────
|
||||
Trip -[FOR_EVENT]-> Event (Personal ↔ Work)
|
||||
Book -[DEVELOPS]-> Skill (Personal ↔ Work)
|
||||
Book -[INFORMS]-> Content (Personal ↔ Work)
|
||||
Infrastructure -[HOSTS]-> Project (Engineering ↔ Work)
|
||||
Prototype -[SUPPORTS]-> Opportunity (Engineering ↔ Work)
|
||||
Project -[GENERATES_REVENUE]-> Account (Work ↔ Personal)
|
||||
Training -[BUILDS]-> Skill (Personal ↔ Work)
|
||||
|
||||
Full schema: docs/neo4j-unified-schema.md
|
||||
════════════════════════════════════════════════════════════════
|
||||
"""
|
||||
print(schema_doc)
|
||||
logger.info("Schema documentation displayed")
|
||||
|
||||
|
||||
def get_credentials(args):
|
||||
"""
|
||||
Collect Neo4j credentials from environment variables, CLI args, or prompts.
|
||||
Priority: CLI args > Environment variables > Interactive prompts
|
||||
"""
|
||||
# URI
|
||||
uri = args.uri or os.environ.get("NEO4J_URI")
|
||||
if not uri:
|
||||
uri = input("Neo4j URI [bolt://localhost:7687]: ").strip()
|
||||
if not uri:
|
||||
uri = "bolt://localhost:7687"
|
||||
|
||||
# Username
|
||||
user = args.user or os.environ.get("NEO4J_USER")
|
||||
if not user:
|
||||
user = input("Neo4j username [neo4j]: ").strip()
|
||||
if not user:
|
||||
user = "neo4j"
|
||||
|
||||
# Password (never from CLI for security)
|
||||
password = os.environ.get("NEO4J_PASSWORD")
|
||||
if not password:
|
||||
password = getpass.getpass("Neo4j password: ")
|
||||
if not password:
|
||||
logger.error("Password is required")
|
||||
sys.exit(1)
|
||||
|
||||
return uri, user, password
|
||||
|
||||
|
||||
def parse_args():
|
||||
"""Parse command line arguments"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Initialize Neo4j Unified Knowledge Graph schema for all AI assistants",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
%(prog)s # Interactive prompts
|
||||
%(prog)s --uri bolt://ariel.incus:7687 # Specify URI, prompt for rest
|
||||
%(prog)s --test-only # Run tests without creating schema
|
||||
%(prog)s --skip-samples # Create schema without sample data
|
||||
|
||||
Environment Variables:
|
||||
NEO4J_URI Bolt connection URI
|
||||
NEO4J_USER Database username
|
||||
NEO4J_PASSWORD Database password (recommended for scripts)
|
||||
|
||||
Schema Reference:
|
||||
docs/neo4j-unified-schema.md
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--uri", "-u",
|
||||
help="Neo4j Bolt URI (default: bolt://localhost:7687)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--user", "-U",
|
||||
help="Neo4j username (default: neo4j)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--test-only", "-t",
|
||||
action="store_true",
|
||||
help="Only run verification tests, don't create schema"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip-samples",
|
||||
action="store_true",
|
||||
help="Skip creating sample nodes"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip-docs",
|
||||
action="store_true",
|
||||
help="Skip displaying schema documentation"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--quiet", "-q",
|
||||
action="store_true",
|
||||
help="Reduce output verbosity"
|
||||
)
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Main execution function.
|
||||
Collects credentials via prompts or environment variables.
|
||||
"""
|
||||
args = parse_args()
|
||||
|
||||
# Set log level
|
||||
if args.quiet:
|
||||
logging.getLogger().setLevel(logging.WARNING)
|
||||
|
||||
# Get credentials
|
||||
uri, user, password = get_credentials(args)
|
||||
|
||||
logger.info(f"Connecting to Neo4j at {uri}...")
|
||||
|
||||
try:
|
||||
schema = LifeGraphSchema(uri, user, password)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create database driver: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
# Verify connection first
|
||||
try:
|
||||
schema.verify_connection()
|
||||
except AuthError:
|
||||
logger.error("✗ Authentication failed - check username/password")
|
||||
sys.exit(1)
|
||||
except ServiceUnavailable:
|
||||
logger.error(f"✗ Cannot connect to Neo4j at {uri}")
|
||||
sys.exit(1)
|
||||
|
||||
if args.test_only:
|
||||
# Just run basic tests (no schema verification)
|
||||
success = schema.run_tests(include_schema_tests=False)
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
# Display schema documentation
|
||||
if not args.skip_docs:
|
||||
schema.document_schema()
|
||||
|
||||
# Create constraints (includes automatic indexes)
|
||||
logger.info("Creating constraints (74 node types)...")
|
||||
schema.create_constraints()
|
||||
|
||||
# Create additional indexes
|
||||
logger.info("Creating indexes...")
|
||||
schema.create_indexes()
|
||||
|
||||
# Create sample nodes to validate schema
|
||||
if not args.skip_samples:
|
||||
logger.info("Creating sample nodes...")
|
||||
schema.create_sample_nodes()
|
||||
|
||||
# Run verification tests (including schema tests)
|
||||
logger.info("Verifying schema...")
|
||||
test_success = schema.run_tests(include_schema_tests=True)
|
||||
|
||||
# Summary
|
||||
stats = schema.verify_schema()
|
||||
logger.info("=" * 60)
|
||||
logger.info("SCHEMA INITIALIZATION COMPLETE")
|
||||
logger.info("=" * 60)
|
||||
logger.info(f" Constraints: {stats['constraints']}")
|
||||
logger.info(f" Indexes: {stats['indexes']}")
|
||||
logger.info(f" Nodes: {stats['nodes']}")
|
||||
logger.info("=" * 60)
|
||||
|
||||
if test_success:
|
||||
logger.info("✓ All tests passed!")
|
||||
logger.info("\nUnified graph ready for all 15 assistants.")
|
||||
logger.info("Schema reference: docs/neo4j-unified-schema.md")
|
||||
logger.info("\nNext steps:")
|
||||
logger.info(" 1. Import data (Plex, Calibre, etc.)")
|
||||
logger.info(" 2. Configure MCP servers for each assistant")
|
||||
logger.info(" 3. Update assistant prompts with unified graph sections")
|
||||
else:
|
||||
logger.warning("⚠ Some tests failed - review output above")
|
||||
sys.exit(1)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
logger.info("\nOperation cancelled by user")
|
||||
sys.exit(130)
|
||||
except Exception as e:
|
||||
logger.error(f"Error during schema initialization: {e}")
|
||||
sys.exit(1)
|
||||
finally:
|
||||
schema.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user