Files
lore-engine-poc/postgres/init.sql
Kay 4f922899af T6: multi-world namespace — world_id on every node, list_worlds(), arda_greyscale seed
Every read tool (entity_context, state_at, was_true_at, ancestors_of,
descendants_of, lineage_of, recall_images, search_images_by_caption,
register_image) now accepts an optional world_id parameter, defaulting
to 'default' so v1 callers see no change.

* Neo4j: every Person/Faction/Location/Item/Event/Lineage/Image node
  carries a world_id string property. seed.py backfills existing nodes
  to 'default' and writes a second minimal world 'arda_greyscale'
  (2 people, 1 faction, 1 location, 4 relations, 1 event, 1 era).
* Cypher: every MATCH in the world/lineage/images plugins filters by
  world_id on the right node type.
* New admin tool list_worlds() — runs
  MATCH (n) WHERE n.world_id IS NOT NULL RETURN DISTINCT n.world_id
  to expose the namespace.
* Postgres image_manifest gains a world_id column (init.sql).
* 4 placeholder images generated for the greyscale world (portraits of
  Mael and Sira, Ashen Hall, the Ashen Oath).
* test.sh now calls every tool with world_id='default' to verify v1
  behaviour, plus a new section 12 that calls list_worlds().
* tests/test_multi_world.py: 14 pytest cases covering list_worlds,
  entity_context/world isolation, was_true_at, state_at, lineage
  filter, image recall isolation, and the image_manifest schema.

Verification:
  pytest tests/test_multi_world.py         14/14 pass
  bash test.sh                             12/12 sections green,
                                           MinIO image bytes flow 200 OK
  list_worlds() returns [{arda_greyscale}, {default}]
2026-06-16 23:09:40 +00:00

55 lines
2.1 KiB
SQL

-- Lore Engine POC — minimal Postgres schema.
-- Operational data that doesn't belong in the world graph.
-- pgvector: 384-dim embeddings for semantic image search.
-- (Requires the `pgvector` image or installed extension on the host OS.)
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE IF NOT EXISTS trade_log (
id BIGSERIAL PRIMARY KEY,
world_id TEXT NOT NULL DEFAULT 'default',
occurred_at TIMESTAMPTZ NOT NULL DEFAULT now(),
in_fiction_time TEXT,
buyer_id TEXT,
seller_id TEXT,
item_id TEXT,
quantity NUMERIC,
unit TEXT,
unit_price NUMERIC,
total_price NUMERIC,
location_id TEXT,
notes TEXT
);
CREATE INDEX IF NOT EXISTS trade_log_time ON trade_log (occurred_at DESC);
CREATE INDEX IF NOT EXISTS trade_log_buyer ON trade_log (buyer_id);
-- Image manifests. The actual bytes live in MinIO; this is metadata + tags
-- that the LLM can query to know what images exist.
CREATE TABLE IF NOT EXISTS image_manifest (
id BIGSERIAL PRIMARY KEY,
image_id TEXT NOT NULL UNIQUE,
world_id TEXT NOT NULL DEFAULT 'default',
object_key TEXT NOT NULL, -- the MinIO object key
entity_id TEXT, -- linked LoreEntity (e.g. Person.id)
entity_type TEXT, -- Person / Location / Event / Item
caption TEXT NOT NULL,
tags TEXT[],
era TEXT,
uploaded_at TIMESTAMPTZ NOT NULL DEFAULT now(),
width INT,
height INT,
bytes BIGINT
);
CREATE INDEX IF NOT EXISTS image_manifest_entity ON image_manifest (entity_id);
CREATE INDEX IF NOT EXISTS image_manifest_tags ON image_manifest USING GIN (tags);
CREATE INDEX IF NOT EXISTS image_manifest_era ON image_manifest (era);
CREATE INDEX IF NOT EXISTS image_manifest_world ON image_manifest (world_id);
-- Image embeddings (pgvector). One row per embedded image. Filled by
-- plugins/embeddings.py `embed_images` (idempotent on image_id).
CREATE TABLE IF NOT EXISTS image_embedding (
image_id TEXT PRIMARY KEY REFERENCES image_manifest(image_id) ON DELETE CASCADE,
embedding vector(384) NOT NULL,
embedded_at TIMESTAMPTZ NOT NULL DEFAULT now()
);