Claude Code Plugins

Community-maintained marketplace

Feedback

>-

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name trait-learning
description Use when working with trait extraction, place enrichment, or the learning loop. Load for understanding how the system learns from gameplay, how traits are extracted from Nominatim + player sessions, async queue processing, and LLM prompt configuration. Covers update_place_traits, pgmq, and edge function flow.

Trait Learning

How the system learns from gameplay and enriches places with traits.

Announce: "I'm using trait-learning to understand the learning mechanism."

Learning Loop Overview

Player completes game
        ↓
    Trigger fires (on correct submission)
        ↓
    Job queued to pgmq.trait_extraction
        ↓
    Edge function called via pg_net
        ↓
    update_place_traits() executes
        ↓
    LLM curates traits from all sources
        ↓
    Place traits replaced with new set

Data Sources for Learning

The LLM receives all available context when extracting traits:

-- 1. Nominatim data (class, type, extratags)
v_nominatim_data := game_logic.fetch_nominatim_place(v_place.osm_id);

-- 2. Session descriptions (what players typed)
SELECT array_agg(DISTINCT description)
FROM game_sessions
WHERE place_id = p_place_id;

-- 3. Game answers (confirmed yes/no from gameplay)
-- Format: "+ trait text" for yes, "- trait text" for no
SELECT array_agg(
  CASE WHEN ga.answer = 'yes' THEN '+ ' || t.clause
       WHEN ga.answer = 'no' THEN '- ' || t.clause
  END
)
FROM game_answers ga
JOIN traits t ON t.id = ga.trait_id;

-- 4. Existing traits (for review/curation)
SELECT array_agg(t.clause)
FROM place_traits pt
JOIN traits t ON t.id = pt.trait_id;

Trait Table Schema

-- Traits are deduplicated by embedding
CREATE TABLE traits (
  id UUID PRIMARY KEY,
  clause TEXT NOT NULL,           -- "Built in 1889"
  embedding_id UUID UNIQUE        -- Links to embeddings table
);

-- Many-to-many link
CREATE TABLE place_traits (
  place_id UUID REFERENCES places,
  trait_id UUID REFERENCES traits,
  PRIMARY KEY (place_id, trait_id)
);

-- Embeddings with source text deduplication
CREATE TABLE embeddings (
  id UUID PRIMARY KEY,
  embedding vector(384),
  source_text TEXT UNIQUE         -- Prevents duplicate embeddings
);

Core Function: update_place_traits

Location: supabase/db/game_logic/functions/places/update_place_traits.sql

-- Key steps:
1. Fetch Nominatim data for place
2. Gather session descriptions
3. Gather game answers (yes/no from gameplay)
4. Get existing traits
5. Build LLM prompt with all context
6. Call LLM via call_llm_api()
7. Parse JSON response {"traits": [...]}
8. DELETE all existing place_traits for this place
9. For each trait:
   - Generate embedding (passage type)
   - Insert trait (ON CONFLICT updates)
   - Link place to trait
10. Update place.pending_review = FALSE

Important: LLM output is authoritative. Existing traits are REPLACED, not merged.

Async Queue Processing

Trait extraction is async to avoid blocking game flow:

-- Queue table via pgmq
SELECT pgmq.create('trait_extraction');

-- Job added after successful game
INSERT INTO pgmq.trait_extraction (message)
VALUES ('{"place_id": "...", "function_name": "update_place_traits"}');

-- Backup processor for orphaned jobs (every 60 seconds)
SELECT cron.schedule(
  'process-orphaned-trait-jobs',
  '60 seconds',
  'SELECT game_logic.process_orphaned_trait_jobs();'
);

Edge Function: process-trait-extraction

Location: supabase/functions/process-trait-extraction/index.ts

// Receives fire-and-forget from pg_net
// Validates whitelisted function (security)
// Calls RPC to execute trait extraction
// Archives queue message on success

const ALLOWED_FUNCTIONS = ['update_place_traits'] as const

// Call the database function
const { error } = await supabase.rpc(function_name, params)

// Clean up queue
await supabase.rpc('archive_trait_queue_by_place', { p_place_id: placeId })

LLM Prompt Configuration

All prompts are in game_logic.config:

-- Key config entries
'llm.trait_extraction.model'           -- Which model to use
'llm.trait_extraction.temperature'     -- Low for precision (0.15)
'llm.trait_extraction.max_traits'      -- Cap per place (20)
'llm.trait_extraction.prompt'          -- Full prompt template

-- Template placeholders
{place_name}           -- Place name
{lat}, {lng}          -- Coordinates
{country}             -- From Nominatim address
{place_type}          -- From Nominatim type
{nominatim_text}      -- Filtered extratags
{existing_traits}     -- Current traits
{session_descriptions} -- Player descriptions
{game_answers}        -- Confirmed yes/no answers
{max_traits}          -- Limit

Trait Quality Guidelines

The LLM prompt includes quality rules:

Good traits:

  • Specific facts: "Built 1889", "Over 300 meters tall"
  • Material/structure: "Made of iron", "Has a lattice structure"
  • Heritage status: "UNESCO World Heritage since 1991"

Bad traits (filtered by prompt):

  • Redundant: Multiple traits testing same property
  • Raw data: URLs, Wikidata IDs, hex colors
  • Useless: Floor count, wheelchair access, hours
  • Generic: "Tourism attraction", "Historic landmark"

Embedding Types

-- Query embeddings (user input)
get_embedding(description, 'query')

-- Passage embeddings (traits being matched)
get_embedding(trait_clause, 'passage')

The input_type affects the embedding model's prefix. Use 'passage' for traits since they're being searched, not searching.

Testing Trait Learning

-- Manually trigger trait extraction
SELECT game_logic.update_place_traits('place-uuid-here');

-- View traits for a place
SELECT t.clause, e.source_text
FROM place_traits pt
JOIN traits t ON t.id = pt.trait_id
LEFT JOIN embeddings e ON e.id = t.embedding_id
WHERE pt.place_id = 'place-uuid-here';

-- Check queue status
SELECT * FROM pgmq.read('trait_extraction', 30, 5);

Anti-Patterns

DON'T: Add Traits Without Embedding

-- WRONG: Trait without embedding won't match
INSERT INTO traits (id, clause)
VALUES (gen_random_uuid(), 'Some trait');

-- CORRECT: Always generate embedding
v_embedding_id := get_embedding(v_clause, 'passage');
INSERT INTO traits (id, clause, embedding_id)
VALUES (gen_random_uuid(), v_clause, v_embedding_id);

DON'T: Merge Instead of Replace

-- WRONG: Merging creates trait bloat
INSERT INTO place_traits ... ON CONFLICT DO NOTHING;

-- CORRECT: LLM curates, we replace
DELETE FROM place_traits WHERE place_id = p_place_id;
-- Then insert new curated set

DON'T: Skip Queue for Sync Processing

-- WRONG: Blocks game completion
SELECT game_logic.update_place_traits(p_place_id);

-- CORRECT: Queue for async processing
SELECT pgmq.send('trait_extraction', 
  jsonb_build_object('place_id', p_place_id));

Related Skills

  • game-scoring - How traits affect candidate scoring
  • postgres-vectors - Embedding storage and similarity
  • edge-functions - Edge function patterns