Files
lore-engine-poc/postgres/init.sql
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

53 lines
2.0 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,
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);
-- 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()
);