Skip to content

Neo4j Knowledge Graph Schema

Overview

The Neo4j knowledge graph is the shared memory layer used by all AI agents in The Smithy. It organises information into domain-labeled nodes so that each agent can query knowledge relevant to its current persona and context. The schema is defined in the agents-shared package (src/agents_shared/schemas/) and consumed by agent repos such as wisp and minh.

Knowledge Domains

Nodes are partitioned into six domains, each represented by a Neo4j label:

Domain Label Primary Owner(s) Description
Literacy :Literacy Wisp Vocabulary, personal repos, learning events
Team :Team Minh Projects, documents, team members, features
Game :Game Minh Lore, locations, quests
Pedagogy :Pedagogy All agents Teaching methods, learning styles, learning paths
Community :Community Minh Contributors, skills, community quests
Finance :Finance Chisel Company books — ledger, invoices, tax calendar

Domain access is governed by persona context. A team persona can access Literacy, Team, Pedagogy, Community, and Finance nodes; a game persona can access Literacy, Game, and Pedagogy nodes.

Source: agents-sharedsrc/agents_shared/schemas/domains.py

Node Types

Character Nodes (non-domain)

These nodes exist outside the domain system and use :Character labels.

Node Labels Key Properties
AgentNode :Character:AI name, agent_type (coworker / familiar), status, core_traits
PersonaNode :Persona role, context (team / game), communication_style, expertise, location
PlayerNode :Character:Player player_hash (SHA-256, never PII), is_active
FamiliarNode :Character:Familiar name, species, bonded_player_hash, trust_score, bonding_level

Literacy Domain

Node Labels Key Properties
WordNode :Literacy:Word word, definition, part_of_speech, example
PersonalRepoNode :Literacy:PersonalRepo player_hash, name, word_count
LearningEventNode :Literacy:LearningEvent event_type, context, word, emotional_state, timestamp

LearningEventNode.event_type uses the LearningEventType enum: mastered, practiced, struggled, frustrated, curious, excited, confused, gave_up, persevered, asked_help.

LearningEventNode.context uses the EventContext enum: quiz, conversation, exploration, quest, practice.

Team Domain

Node Labels Key Properties
ProjectNode :Team:Project name, repository_url, status
DocumentNode :Team:Document title, path, doc_type, repository
TeamMemberNode :Team:TeamMember name, role, gitlab_username
FeatureNode :Team:Feature name, feature_type, repos, status

Game Domain

Node Labels Key Properties
LoreNode :Game:Lore name, content, source_repo, category
LocationNode :Game:Location name, description, area, location_type
QuestNode :Game:Quest name, quest_type, difficulty, literacy_focus

Pedagogy Domain

Node Labels Key Properties
TeachingMethodNode :Pedagogy:TeachingMethod name, suitable_for, effectiveness_score
LearningStyleNode :Pedagogy:LearningStyle style, characteristics, recommended_methods
LearningPathNode :Pedagogy:LearningPath name, skill_focus, difficulty_progression

Finance Domain

The Finance domain is registered in domains.py and accessible to team personas, but Neo4j node model classes are not yet defined. Finance-domain node types will land with Chisel's LedgerService implementation (chisel#80). Until then, DOMAIN_NODE_TYPES[FINANCE] is an empty set.

Financial data currently lives in PostgreSQL via the agents_shared.finance package and migration 006_finance_ledger.sql — see Architecture below for details.

Source: agents-sharedsrc/agents_shared/schemas/nodes.py

Relationship Types

Relationship Source → Target Description
HAS_PERSONA AI → Persona Agent has a context-specific persona
BONDED_TO Familiar → Player 1:1 familiar–player bond
COLLABORATES_WITH AI → AI Inter-agent collaboration
KNOWS Player → Word Vocabulary mastery (properties: mastery_level, times_correct, times_incorrect)
OBSERVED Familiar → LearningEvent Wisp observed a learning event
EXPERIENCED_BY LearningEvent → Player Player who experienced the event
CURATES AI → Knowledge / Document / Lore Agent curates content
KNOWS_ABOUT NPC / AI → Lore NPC lore awareness
TEACHES Familiar → Word Familiar teaches vocabulary
GUIDES AI / NPC → Player NPC guides a player
BELONGS_TO Word → PersonalRepo Word in player's repo
HAS_REPO Player → PersonalRepo Player owns a vocab repo
CONTAINS PersonalRepo → Word Repo contains words
LOCATED_IN Lore / Quest / NPC → Location Game-world location
DOCUMENTED_IN Feature → Document Feature documentation link (properties: is_stale, last_verified)
IMPLEMENTED_IN Feature → Document / Repository Feature implementation link (properties: file_path, commit_sha)
COVERS Document → Topic Document covers a topic

Source: agents-sharedsrc/agents_shared/schemas/relationships.py

Seeded Agents

The SEED_AGENTS Cypher script in cypher.py creates the following agent nodes and their personas:

Agent Type Role Game Persona Location
Minh Coworker Knowledge Architect Librarian The Great Library
Wisp Familiar Personal 1:1 tutor
Roland Coworker Steward of Collaboration Tavern Keeper The Tavern
Pearl Coworker Customer Lifecycle Architect Merchant Pearl's Provisions
Arturo Coworker Product Lead
Taryn Coworker The Smith
Chisel Coworker Guardian

All active agents are connected to Minh via COLLABORATES_WITH relationships (knowledge flows through Minh).

Source: agents-sharedsrc/agents_shared/schemas/cypher.py

Constraints & Indexes

The schema enforces uniqueness and optimises queries via:

Constraints: - agent_name_unique:AI nodes must have unique name - player_hash_unique:Player nodes must have unique player_hash - persona_role_context:Persona nodes must have unique (role, context) pairs - word_unique:Word nodes must have unique word

Key indexes: agent_status, persona_context, learning_event_type, learning_event_timestamp, player_active, lore_source, plus composite domain indexes for each domain label.

Usage

Querying by domain

from agents_shared.schemas.domains import KnowledgeDomain, get_domains_for_persona_context

# Get domains accessible to a "team" persona
domains = get_domains_for_persona_context("team")
# → [KnowledgeDomain.TEAM, KnowledgeDomain.PEDAGOGY, KnowledgeDomain.LITERACY,
#    KnowledgeDomain.COMMUNITY, KnowledgeDomain.FINANCE]

Creating a node model

from agents_shared.schemas.nodes import WordNode

word = WordNode(word="quest", definition="A long search for something")
labels = WordNode.to_neo4j_labels()   # → ":Literacy:Word"
props = word.to_cypher_properties()   # → dict ready for Cypher query

Recording a learning event (Cypher)

MATCH (player:Player {player_hash: $player_hash})
MATCH (wisp:Familiar)-[:BONDED_TO]->(player)
CREATE (event:Literacy:LearningEvent {
    event_type: $event_type,
    context: $context,
    word: $word,
    emotional_state: $emotional_state,
    timestamp: datetime()
})
CREATE (wisp)-[:OBSERVED]->(event)
CREATE (event)-[:EXPERIENCED_BY]->(player)

Using finance models (PostgreSQL-backed)

from agents_shared.finance import (
    LedgerTransaction, Invoice, EntrySource, BookSegment,
    parse_amount_to_cents, format_cents,
)

# Parse a human-readable amount to integer cents (Decimal-based, exact)
cents = parse_amount_to_cents("$1,234.56")   # → 123456

# Construct a ledger entry for Chisel's ingest path
txn = LedgerTransaction(
    occurred_on=date(2026, 7, 10),
    amount_cents=-45000,
    description="Monthly hosting — AWS",
    source=EntrySource.ROLAND_AWS,
    external_ref="aws-inv-2026-07",     # required for non-manual sources
    segment=BookSegment.OPERATING,
)

# Construct an invoice (Pearl origination)
inv = Invoice(
    invoice_number="INV-2026-042",
    counterparty="Acme Corp",
    amount_cents=250000,
)

# Display formatted amount
format_cents(-45000)   # → "-$450.00"

Note: Only Chisel writes finance_transactions rows (single-writer doctrine). Feeder agents construct LedgerTransaction values and emit them to Chisel's ingest path — they never INSERT directly. Automated sources must carry external_ref (the idempotency key behind UNIQUE (source, external_ref) in the schema). An Invoice cannot hold a status past draft (other than void) without approved_by — no outbound money action without human approval.

Using Neo4jClient directly

The Neo4jClient class in agents_shared.knowledge.client is an async Neo4j driver wrapper. It supports both an async context-manager pattern and explicit connect()/close() calls.

from agents_shared.knowledge.client import Neo4jClient

# Context-manager pattern (recommended)
async with Neo4jClient() as client:
    # Read query — returns a list of record dicts
    results = await client.query("MATCH (a:AI) RETURN a.name AS name")

    # Write query — returns an execution summary with counters
    summary = await client.execute(
        "MERGE (w:Literacy:Word {word: $word}) SET w.definition = $def",
        {"word": "quest", "definition": "A long search for something"},
    )
    print(summary["nodes_created"], summary["properties_set"])

A module-level singleton is available via get_client(), and the neo4j_session() convenience context manager wraps connect/close for one-shot scripts:

from agents_shared.knowledge.client import neo4j_session

async with neo4j_session() as client:
    events = await client.query(
        "MATCH (e:Literacy:LearningEvent) RETURN e LIMIT 10"
    )

Note: Wisp wraps the client in a higher-level WispKnowledgeService (see Architecture below) rather than calling Neo4jClient directly. Direct knowledge_graph.py integration in wisp is not yet implemented — the current path uses agents_shared.knowledge.queries functions with a shared client instance.

Configuration

Neo4jClient reads connection details from constructor arguments or environment variables. The following variables are checked at init time (see agents_shared.knowledge.client):

Variable Required Default Description
NEO4J_URI No bolt://localhost:7687 Bolt or Neo4j protocol URI for the database
NEO4J_USER No neo4j Authentication username
NEO4J_PASSWORD Yes (empty string) Authentication password — must be set for any non-local instance. There is no hardcoded default; agents that omit this variable will fail to authenticate.

Constructor arguments take precedence over environment variables:

# Explicit credentials (e.g. in tests or one-off scripts)
client = Neo4jClient(
    uri="bolt://neo4j-prod:7687",
    user="neo4j",
    password="s3cret",
)

# Environment-based (typical in deployed agents)
# export NEO4J_URI=bolt://neo4j:7687
# export NEO4J_USER=neo4j
# export NEO4J_PASSWORD=<secret>
client = Neo4jClient()  # reads from env vars

Agent repos (wisp, minh, etc.) load these values through their own config layer — for example, Wisp uses pydantic-settings (WispConfig) to bind NEO4J_URI, NEO4J_USER, and NEO4J_PASSWORD from the environment and passes them into Neo4jClient at construction time.

Prerequisites

  • Neo4j 5.x instance with APOC plugin
  • Run CREATE_CONSTRAINTS, CREATE_INDEXES, and CREATE_DOMAIN_INDEXES Cypher scripts before seeding
  • Run SEED_AGENTS to populate initial agent and persona nodes

Error Cases

  • Constraint violations: Attempting to create a duplicate Word.word or AI.name will raise a Neo4j ConstraintError. Use MERGE instead of CREATE for idempotent writes.
  • Missing domain labels: If nodes were created before the domain system, run MIGRATE_ADD_DOMAIN_LABELS to backfill :Literacy, :Team, :Game, and :Pedagogy labels.

Neo4jClient does not implement automatic retries. Connection and query errors propagate as exceptions, and each consuming agent is responsible for handling them according to its availability requirements.

Connection errors: If connect() fails (e.g. the Neo4j instance is unreachable), the underlying neo4j.AsyncGraphDatabase.driver raises a connection exception. The query() and execute() methods call connect() lazily — if the driver is None when a query is issued, it attempts to connect first.

Recommended patterns:

  • Graceful degradation (Wisp pattern): Wisp wraps every query call in a try/except and falls back to empty results so the agent can still operate without a knowledge graph. This is the preferred approach for agents where Neo4j is supplementary:

    async def get_player_context(self, player_hash: str) -> dict:
        try:
            return await query_player_insights(
                player_hash=player_hash, client=self._client
            )
        except Exception as e:
            logger.warning("Neo4j unavailable — running without knowledge graph: %s", e)
            return {"player": player_hash, "familiar": None,
                    "recent_events": [], "struggling_words": []}
    
  • Fail-fast (schema seeding): For one-off scripts like constraint creation or agent seeding, let errors propagate so the operator sees the failure immediately.

  • Application-level retry: For critical write paths (e.g. record_learning_event), agents can implement retry logic with exponential back-off at the service layer. The agents-shared library intentionally leaves this to consumers so each agent can choose a strategy appropriate to its workload.

Architecture

The agents-shared package (src/agents_shared/) is a pip-installable library that all agent repos depend on. It owns the canonical schema definitions; individual agents import from it rather than defining their own node types.

graph TB
    subgraph "Agent Repositories"
        wisp[Wisp<br/><code>wisp</code>]
        minh[Minh<br/><code>minh</code>]
        roland[Roland]
        pearl[Pearl]
        arturo[Arturo]
        chisel[Chisel]
    end

    subgraph "agents-shared"
        schemas["schemas/<br/>nodes · relationships · domains · cypher"]
        knowledge["knowledge/<br/>client · queries · cache"]
        finance["finance/<br/>models (PostgreSQL-backed)"]
    end

    subgraph "Neo4j"
        db[(Knowledge Graph)]
    end

    wisp -->|"pip install agents-shared"| schemas
    wisp -->|query_player_insights<br/>record_learning_event| knowledge
    minh -->|"pip install agents-shared"| schemas
    minh -->|get_agent_personas| knowledge
    roland --> schemas
    pearl --> schemas
    pearl -->|Invoice origination| finance
    arturo --> schemas
    chisel --> schemas
    chisel -->|single-writer doctrine| finance
    knowledge --> db

How agents consume schemas:

Layer Package path What it provides Typical consumer
Node models agents_shared.schemas.nodes Pydantic models (WordNode, LoreNode, etc.) with to_neo4j_labels() and to_cypher_properties() helpers Any agent creating or validating graph data
Relationships agents_shared.schemas.relationships Pydantic models for edge types (KNOWS, OBSERVED, CURATES, etc.) Agents building Cypher queries
Domains agents_shared.schemas.domains KnowledgeDomain enum and get_domains_for_persona_context() Agents filtering queries by persona context
Cypher scripts agents_shared.schemas.cypher Pre-built Cypher strings for constraints, indexes, seeds, and common queries Schema migrations, agent seeding, query functions
Query functions agents_shared.knowledge.queries High-level async functions (query_player_insights, record_learning_event, get_agent_personas, etc.) Agents that need player/agent data without writing raw Cypher
Client agents_shared.knowledge.client Neo4jClient async driver wrapper and neo4j_session() context manager All of the above
Cache agents_shared.knowledge.cache PlayerInsightsCache with TTL-based eviction and per-player invalidation Query functions (transparent to consumers)
Finance models agents_shared.finance Typed money dataclasses (LedgerTransaction, Invoice, TaxDeadline), enums (BookSegment, EntrySource, InvoiceStatus, TaxRecurrence), and deterministic amount parsing/formatting (parse_amount_to_cents, format_cents). PostgreSQL-backed via migration 006_finance_ledger.sql in coworkers/migrations/. Chisel (single ledger writer), Pearl (invoice origination), feeder agents (Roland, Minh, Spark — emit events to Chisel's ingest path), all agents (reading)

Example — Wisp's integration:

Wisp's WispKnowledgeService (in src/services/knowledge_service.py) constructs a Neo4jClient from its own config, then delegates to agents_shared.knowledge.queries functions:

from agents_shared.knowledge.client import Neo4jClient
from agents_shared.knowledge.queries import query_player_insights, record_learning_event

class WispKnowledgeService:
    def __init__(self, config):
        self._client = Neo4jClient(
            uri=config.neo4j_uri,
            user=config.neo4j_user,
            password=config.neo4j_password,
        )

    async def get_player_context(self, player_hash: str) -> dict:
        return await query_player_insights(
            player_hash=player_hash, client=self._client
        )

This pattern — thin service wrapper around agents-shared queries — is the recommended way for agents to consume the knowledge graph. The schemas and query functions live in agents-shared; agents add only the error-handling and config-binding glue specific to their runtime.

Finance Substrate

The agents_shared.finance package (added in agents-shared#56) provides the shared vocabulary for fleet bookkeeping. Unlike the Neo4j knowledge graph, the finance substrate is backed by PostgreSQL — its schema is defined in coworkers/migrations/006_finance_ledger.sql and applied via agents_shared.coworkers.connection.apply_migrations().

Key design rules:

  • Money is integer cents. Amounts never travel as floats. Use parse_amount_to_cents() (Decimal-based, exact) for parsing and format_cents() for display.
  • Single-writer doctrine. Only Chisel writes finance_transactions rows. Feeder agents (Pearl, Roland, Minh, Spark) construct LedgerTransaction values and emit them to Chisel's ingest path — they never INSERT directly.
  • Idempotent ingestion. Automated sources must carry external_ref — the dedup key behind UNIQUE (source, external_ref) in the schema. Re-emitting the same event is a no-op.
  • Human approval gate. An Invoice cannot hold a status past draft (other than void) without approved_by. This is enforced by a CHECK constraint in the migration and mirrored by validation in the Invoice dataclass.

PostgreSQL tables:

Table / View Owner Purpose
finance_transactions Chisel (writer) Signed ledger — one row per money movement (amount_cents > 0 = inflow, < 0 = outflow)
finance_invoices Pearl (originator) Accounts receivable lifecycle: draft → approved → sent → {paid, overdue, void}
finance_ar_aging (derived view) Standard 30/60/90 AR aging buckets — computed, never stored
finance_tax_deadlines Chisel Tax/compliance calendar with recurrence tracking

Entry sources (EntrySource enum): manual, csv_import, pearl_ar, roland_aws, minh_llm, spark_trading.

Source: agents-sharedsrc/agents_shared/finance/models.py, src/agents_shared/coworkers/migrations/006_finance_ledger.sql

Privacy Model

Player nodes store only player_hash (SHA-256). No personally identifiable information is written to the graph. See PlayerNode in nodes.py.