- 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)
126 lines
4.4 KiB
Python
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
|