Files
lore-engine-poc/plugins/consistency.py
Hermes add264eb04 T2: pgvector image embeddings — plugin, schema, seed, hook, tests
- docker-compose: swap postgres image to pgvector/pgvector:pg16
- postgres/init.sql: CREATE EXTENSION vector; image_embedding table
- plugins/embeddings.py: embed_images + search_images_semantic
  (sentence-transformers all-MiniLM-L6-v2, lazy-loaded, pgvector <=> cosine)
- plugins/images.py: register_image kicks off background embed worker
- seed.py: seed_embeddings writes 4 embeddings for the mock images
- README: semantic image search section + T3 note
- 11 tests across 4 files, all green:
    test_embeddings_plugin.py (4): schema, ordering, idempotency, stub
    test_embeddings_real_model.py (3): real MiniLM, acceptance queries
    test_register_image_hook.py (2): manifest row, end-to-end hook
    test_seed_embeddings.py (2): writes 4, idempotent
- Includes T3 consistency plugin skeleton (4 stub tools)
2026-06-16 14:30:10 +00:00

126 lines
4.4 KiB
Python

"""
consistency plugin — violation detection surface.
Tools (all stubbed for v2.T3; real implementations land in T5):
- find_contradictions(severity): find Contradiction nodes.
- find_anachronisms(severity): find Anachronism nodes.
- find_orphans(): find Orphan nodes.
- find_ontology_violations(): find OntologyViolation nodes.
Each tool returns a stubbed {"violations": [], "count": 0} today. T5 wires
the actual detection rules per lore-engine/docs/04-consistency.md.
"""
from server import get_neo4j, REGISTRY
def _q(query, params=None):
"""Run a single read query against Neo4j, return list of dicts."""
driver = get_neo4j()
with driver.session() as s:
result = s.run(query, params or {})
return [dict(r) for r in result]
def _empty():
"""Stub envelope shared by all 4 tools until T5 wires the real Cypher."""
return {"violations": [], "count": 0}
@REGISTRY.tool(
name="find_contradictions",
description="Find Contradiction nodes in the world graph. A contradiction is two sources making incompatible claims about the same fact (e.g. conflicting RULES, MEMBER_OF, POSSESSES). Optionally filter by severity ('error' or 'warn').",
input_schema={
"type": "object",
"properties": {
"severity": {
"type": "string",
"enum": ["any", "error", "warn"],
"default": "any",
"description": "Filter by severity. 'any' (default) returns all.",
},
},
},
)
def find_contradictions(args):
severity = args.get("severity", "any")
if severity == "any":
cypher = "MATCH (v:Contradiction) RETURN v ORDER BY v.detected_at DESC"
params = {}
else:
cypher = "MATCH (v:Contradiction {severity: $severity}) RETURN v ORDER BY v.detected_at DESC"
params = {"severity": severity}
_q(cypher, params) # statement must run so a real T5 swap is a no-op
return _empty()
@REGISTRY.tool(
name="find_anachronisms",
description="Find Anachronism nodes: claims that require a person/faction/thing to exist at a time it could not have (e.g. Aldric at a battle 200 years before his birth). Optionally filter by severity.",
input_schema={
"type": "object",
"properties": {
"severity": {
"type": "string",
"enum": ["any", "error", "warn"],
"default": "any",
"description": "Filter by severity. 'any' (default) returns all.",
},
},
},
)
def find_anachronisms(args):
severity = args.get("severity", "any")
if severity == "any":
cypher = "MATCH (v:Anachronism) RETURN v ORDER BY v.detected_at DESC"
params = {}
else:
cypher = "MATCH (v:Anachronism {severity: $severity}) RETURN v ORDER BY v.detected_at DESC"
params = {"severity": severity}
_q(cypher, params)
return _empty()
@REGISTRY.tool(
name="find_orphans",
description="Find Orphan nodes: entities the graph has no link to anything else (Person with no recorded parents, Location not in any Region, Faction with no FOUNDED event). Surfaced as gaps, not asserted errors.",
input_schema={
"type": "object",
"properties": {},
},
)
def find_orphans(args):
_q("MATCH (v:Orphan) RETURN v ORDER BY v.detected_at DESC")
return _empty()
@REGISTRY.tool(
name="find_ontology_violations",
description="Find OntologyViolation nodes: graph states that violate the world's domain rules (a region inside two non-overlapping kingdoms, a spell in a magic system that does not exist in this era). Optionally filter by severity.",
input_schema={
"type": "object",
"properties": {
"severity": {
"type": "string",
"enum": ["any", "error", "warn"],
"default": "any",
"description": "Filter by severity. 'any' (default) returns all.",
},
},
},
)
def find_ontology_violations(args):
severity = args.get("severity", "any")
if severity == "any":
cypher = "MATCH (v:OntologyViolation) RETURN v ORDER BY v.detected_at DESC"
params = {}
else:
cypher = "MATCH (v:OntologyViolation {severity: $severity}) RETURN v ORDER BY v.detected_at DESC"
params = {"severity": severity}
_q(cypher, params)
return _empty()
def register(registry):
"""Plugin entry point — server.py calls this. Decorators do the work."""
pass