02 — Data Model
Insurance Intelligence System — PostgreSQL Schema
Zasady projektowania bazy
- Surowe dane nigdy nie są nadpisywane — raw_signals to append-only
- Klasyfikacja jest oddzielna od danych — można reklasyfikować bez utraty oryginału
- Każdy rekord ma źródło i timestamp — pełna audytowalność
- JSONB dla danych surowych — elastyczność bez zmiany schematu przy nowych źródłach
- Segmenty jako enum — spójność i możliwość filtrowania
Schemat bazy danych
Tabela: raw_signals
Surowe dane ze wszystkich źródeł. Append-only — nigdy nie modyfikowana.
CREATE TABLE raw_signals (
id SERIAL PRIMARY KEY,
source_type VARCHAR(50) NOT NULL,
-- 'youtube' | 'knf' | 'piu' | 'nbp' | 'podcast' | 'media'
source_id VARCHAR(200),
-- np. video_id dla YouTube, URL dla stron
collected_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
published_at DATE,
title TEXT,
raw_data JSONB NOT NULL,
-- pełne dane z API / scraper w oryginalnej formie
description_urls TEXT[],
-- URL-e wyciągnięte z opisów (linki do raportów itp.)
checksum VARCHAR(64) UNIQUE
-- SHA256 z source_id — zapobiega duplikatom
);
CREATE INDEX idx_raw_signals_source ON raw_signals(source_type, collected_at);
CREATE INDEX idx_raw_signals_published ON raw_signals(published_at);
Tabela: classified_signals
Wyniki klasyfikacji przez Agenta 2. Jeden rekord na każdy raw_signal.
CREATE TYPE signal_segment AS ENUM (
'ceo_cfo_tu',
'financials_tu',
'regulators',
'conferences',
'bancassurance',
'intermediaries',
'expert_analysis',
'eu_groups',
'noise',
'unclassified'
);
CREATE TYPE classification_method AS ENUM (
'rules', -- sklasyfikowany przez reguły
'claude_api', -- sklasyfikowany przez Claude API
'manual' -- poprawiony ręcznie przez właściciela projektu
);
CREATE TABLE classified_signals (
id SERIAL PRIMARY KEY,
raw_signal_id INTEGER NOT NULL REFERENCES raw_signals(id),
segment signal_segment NOT NULL,
relevance_score SMALLINT CHECK (relevance_score BETWEEN 0 AND 100),
is_noise BOOLEAN NOT NULL DEFAULT FALSE,
classification_method classification_method NOT NULL,
classification_reason TEXT,
-- krótkie uzasadnienie (szczeg. dla claude_api i manual)
companies_mentioned TEXT[],
-- ['PZU', 'Warta'] — TU wymienione w tytule/opisie
roles_mentioned TEXT[],
-- ['prezes', 'dyrektor sprzedaży'] — stanowiska
topics TEXT[],
-- ['wyniki finansowe', 'strategia', 'AI']
classified_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
manually_reviewed_at TIMESTAMPTZ,
manually_reviewed_by VARCHAR(100)
);
CREATE INDEX idx_classified_segment ON classified_signals(segment, relevance_score DESC);
CREATE INDEX idx_classified_noise ON classified_signals(is_noise);
CREATE INDEX idx_classified_companies ON classified_signals USING GIN(companies_mentioned);
Tabela: insights
Wyniki analizy przez Agenta 3. Jeden insight może łączyć wiele sygnałów.
CREATE TABLE insights (
id SERIAL PRIMARY KEY,
signal_ids INTEGER[] NOT NULL,
-- powiązane classified_signals.id
insight_type VARCHAR(50),
-- 'trend' | 'anomaly' | 'competitor_move' | 'regulatory_change'
title TEXT NOT NULL,
summary TEXT NOT NULL,
companies TEXT[],
topics TEXT[],
importance SMALLINT CHECK (importance BETWEEN 1 AND 5),
-- 5 = krytyczny, 1 = informacyjny
period_start DATE,
period_end DATE,
generated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
report_id INTEGER
-- opcjonalne powiązanie z raportem
);
Tabela: known_sources
Źródła które system już zidentyfikował jako wartościowe (kanały YouTube, strony itp.)
CREATE TABLE known_sources (
id SERIAL PRIMARY KEY,
source_type VARCHAR(50) NOT NULL,
source_id VARCHAR(200) NOT NULL UNIQUE,
-- np. channel_id dla YouTube
name TEXT NOT NULL,
description TEXT,
quality_score SMALLINT DEFAULT 50,
-- 0-100, rośnie z każdym wartościowym sygnałem
segment signal_segment,
-- dominujący segment tego źródła
is_monitored BOOLEAN DEFAULT TRUE,
first_seen_at TIMESTAMPTZ DEFAULT NOW(),
last_signal_at TIMESTAMPTZ,
signal_count INTEGER DEFAULT 0
);
Tabela: reports
Wygenerowane raporty tygodniowe / miesięczne / na żądanie.
CREATE TABLE reports (
id SERIAL PRIMARY KEY,
report_type VARCHAR(50),
-- 'weekly' | 'monthly' | 'adhoc' | 'benchmark'
title TEXT NOT NULL,
period_start DATE,
period_end DATE,
content_html TEXT,
content_json JSONB,
generated_at TIMESTAMPTZ DEFAULT NOW(),
generated_by VARCHAR(50) DEFAULT 'agent_reporter'
);
Przepływ danych
YouTube API
└─► raw_signals (source_type='youtube', raw_data={...})
└─► classified_signals (segment, relevance_score, companies[])
└─► insights (trend/anomaly/move)
└─► reports (weekly/monthly/adhoc)
known_sources ◄── aktualizowany gdy nowy kanał pojawia się wielokrotnie
Konwencje
| Element | Konwencja |
|---|---|
| Nazwy tabel | snake_case, liczba mnoga |
| Nazwy kolumn | snake_case |
| Timestampy | zawsze TIMESTAMPTZ (UTC) |
| Daty publikacji | DATE (bez czasu) |
| Dane surowe | JSONB (nie TEXT) |
| Enumy | zdefiniowane jako PostgreSQL TYPE |
| Indeksy | na każdej kolumnie używanej w WHERE/JOIN |