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-shared → src/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-shared → src/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-shared → src/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-shared → src/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_transactionsrows (single-writer doctrine). Feeder agents constructLedgerTransactionvalues and emit them to Chisel's ingest path — they never INSERT directly. Automated sources must carryexternal_ref(the idempotency key behindUNIQUE (source, external_ref)in the schema). AnInvoicecannot hold a status pastdraft(other thanvoid) withoutapproved_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 callingNeo4jClientdirectly. Directknowledge_graph.pyintegration in wisp is not yet implemented — the current path usesagents_shared.knowledge.queriesfunctions 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, andCREATE_DOMAIN_INDEXESCypher scripts before seeding - Run
SEED_AGENTSto populate initial agent and persona nodes
Error Cases¶
- Constraint violations: Attempting to create a duplicate
Word.wordorAI.namewill raise a Neo4jConstraintError. UseMERGEinstead ofCREATEfor idempotent writes. - Missing domain labels: If nodes were created before the domain system, run
MIGRATE_ADD_DOMAIN_LABELSto backfill:Literacy,:Team,:Game, and:Pedagogylabels.
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. Theagents-sharedlibrary 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 andformat_cents()for display. - Single-writer doctrine. Only Chisel writes
finance_transactionsrows. Feeder agents (Pearl, Roland, Minh, Spark) constructLedgerTransactionvalues and emit them to Chisel's ingest path — they never INSERT directly. - Idempotent ingestion. Automated sources must carry
external_ref— the dedup key behindUNIQUE (source, external_ref)in the schema. Re-emitting the same event is a no-op. - Human approval gate. An
Invoicecannot hold a status pastdraft(other thanvoid) withoutapproved_by. This is enforced by aCHECKconstraint in the migration and mirrored by validation in theInvoicedataclass.
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-shared → src/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.