RAGDebugger
bootstrappedRAGDebugger — Spécification Technique MVP
1. Vue d'ensemble
Concept : RAGDebugger est une plateforme d'observabilité SaaS pour pipelines RAG (Retrieval-Augmented Generation). Elle instrumente automatiquement LangChain, LlamaIndex et pipelines custom via un SDK léger, trace chaque étape (embedding → retrieval → reranking → génération) et expose des métriques de qualité avec alertes. Valeur ajoutée principale : Remplacer lesprint() et logs épars par une interface de débogage centralisée qui révèle pourquoi un pipeline RAG hallucine ou retourne des résultats non pertinents — en quelques minutes, pas en jours.
Ce qu'on build en MVP (48h max) :
- SDK Python avec décorateur
@ragdebugger.trace()qui envoie des traces via HTTP - Ingestion des traces via API REST sécurisée par clé API
- Dashboard avec liste/détail de traces, métriques de base (latence, scores similarité)
- Alertes email sur seuils configurables (hallucination score, latence)
- Auth + billing Stripe (Free / Pro)
2. Stack technique
Core
| Couche | Technologie |
|--------|-------------|
| Framework | Next.js 15 App Router + TypeScript strict |
| Base de données | Supabase (PostgreSQL + Auth + Realtime) |
| Billing | Stripe (Checkout + Webhooks + Customer Portal) |
| Emails | Resend + React Email |
| Déploiement | Vercel |
Dépendances NPM spécifiques
{
"dependencies": {
"@supabase/supabase-js": "^2.45.0",
"@supabase/ssr": "^0.5.0",
"stripe": "^16.0.0",
"@stripe/stripe-js": "^4.0.0",
"resend": "^4.0.0",
"@react-email/components": "^0.0.22",
"recharts": "^2.12.0",
"date-fns": "^3.6.0",
"zod": "^3.23.0",
"zustand": "^4.5.0",
"@tanstack/react-query": "^5.51.0",
"@tanstack/react-table": "^8.19.0",
"react-json-view-lite": "^1.4.0",
"nanoid": "^5.0.0",
"clsx": "^2.1.0",
"tailwind-merge": "^2.4.0",
"lucide-react": "^0.400.0",
"@radix-ui/react-dialog": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-select": "^2.1.0",
"@radix-ui/react-tooltip": "^1.1.0",
"@radix-ui/react-badge": "^1.0.0",
"@radix-ui/react-switch": "^1.1.0",
"sonner": "^1.5.0",
"next-themes": "^0.3.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"tsx": "^4.16.0"
}
}
> Note SDK Python (hors Next.js) : Le SDK Python (ragdebugger-sdk) est un package PyPI minimal avec requests ou httpx. Il est livré séparément dans /sdk/python/. La spec en détaille l'interface mais pas l'implémentation interne.
3. Modèle de données Supabase
Tables existantes (présupposées)
profiles— reliée àauth.users, contientid,email,full_name,avatar_url,created_atsubscription_events— log des événements Stripe
Nouvelles tables
-- ============================================================-- ORGANISATIONS (multi-tenant)
-- ============================================================
CREATE TABLE organizations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
slug TEXT NOT NULL UNIQUE,
owner_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
plan TEXT NOT NULL DEFAULT 'free' CHECK (plan IN ('free', 'pro', 'enterprise')),
stripe_customer_id TEXT UNIQUE,
stripe_subscription_id TEXT UNIQUE,
trial_ends_at TIMESTAMPTZ DEFAULT (NOW() + INTERVAL '14 days'),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_organizations_owner_id ON organizations(owner_id);
CREATE INDEX idx_organizations_slug ON organizations(slug);
-- ============================================================
-- MEMBRES D'ORGANISATION
-- ============================================================
CREATE TABLE organization_members (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
role TEXT NOT NULL DEFAULT 'member' CHECK (role IN ('owner', 'admin', 'member')),
invited_email TEXT,
accepted_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(organization_id, user_id)
);
CREATE INDEX idx_org_members_user_id ON organization_members(user_id);
CREATE INDEX idx_org_members_org_id ON organization_members(organization_id);
-- ============================================================
-- PROJETS (un org peut avoir plusieurs pipelines RAG)
-- ============================================================
CREATE TABLE projects (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
name TEXT NOT NULL,
description TEXT,
framework TEXT CHECK (framework IN ('langchain', 'llamaindex', 'custom', 'other')),
created_by UUID NOT NULL REFERENCES profiles(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_projects_org_id ON projects(organization_id);
-- ============================================================
-- CLÉS API
-- ============================================================
CREATE TABLE api_keys (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
project_id UUID REFERENCES projects(id) ON DELETE SET NULL,
name TEXT NOT NULL,
key_hash TEXT NOT NULL UNIQUE, -- SHA-256 de la clé réelle
key_prefix TEXT NOT NULL, -- ex: "rdbg_live_xxxx" (8 premiers chars)
last_used_at TIMESTAMPTZ,
expires_at TIMESTAMPTZ,
revoked_at TIMESTAMPTZ,
created_by UUID NOT NULL REFERENCES profiles(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_api_keys_org_id ON api_keys(organization_id);
CREATE INDEX idx_api_keys_key_hash ON api_keys(key_hash);
-- ============================================================
-- TRACES (enregistrement principal d'une exécution RAG)
-- ============================================================
CREATE TABLE traces (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
-- Identification
trace_id TEXT NOT NULL, -- ID côté SDK (pour corrélation)
session_id TEXT, -- grouper plusieurs turns d'une conversation
user_id_external TEXT, -- ID utilisateur final (côté client)
tags TEXT[] DEFAULT '{}',
-- Requête
query TEXT NOT NULL, -- question posée par l'utilisateur
query_embedding_model TEXT, -- ex: "text-embedding-3-small"
-- Résultat
final_answer TEXT, -- réponse générée par le LLM
llm_model TEXT, -- ex: "gpt-4o-mini"
-- Métriques globales
total_latency_ms INTEGER,
retrieval_latency_ms INTEGER,
llm_latency_ms INTEGER,
total_tokens INTEGER,
prompt_tokens INTEGER,
completion_tokens INTEGER,
estimated_cost_usd NUMERIC(10, 6),
-- Scores calculés
faithfulness_score NUMERIC(4,3), -- 0.000 à 1.000 (réponse fondée sur les chunks ?)
answer_relevance_score NUMERIC(4,3), -- réponse pertinente par rapport à la question ?
context_precision NUMERIC(4,3), -- chunks récupérés pertinents ?
context_recall NUMERIC(4,3),
-- État
status TEXT NOT NULL DEFAULT 'success' CHECK (status IN ('success', 'error', 'timeout', 'partial')),
error_message TEXT,
has_hallucination_flag BOOLEAN DEFAULT FALSE,
-- Metadata
sdk_version TEXT,
environment TEXT DEFAULT 'production' CHECK (environment IN ('production', 'staging', 'development')),
metadata JSONB DEFAULT '{}',
ingested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -- timestamp côté SDK
);
CREATE INDEX idx_traces_project_id ON traces(project_id);
CREATE INDEX idx_traces_org_id ON traces(organization_id);
CREATE INDEX idx_traces_ingested_at ON traces(ingested_at DESC);
CREATE INDEX idx_traces_trace_id ON traces(trace_id);
CREATE INDEX idx_traces_session_id ON traces(session_id) WHERE session_id IS NOT NULL;
CREATE INDEX idx_traces_status ON traces(status);
CREATE INDEX idx_traces_has_hallucination ON traces(has_hallucination_flag) WHERE has_hallucination_flag = TRUE;
CREATE INDEX idx_traces_tags ON traces USING GIN(tags);
-- ============================================================
-- SPANS (étapes individuelles dans une trace)
-- ============================================================
CREATE TABLE spans (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
trace_id_fk UUID NOT NULL REFERENCES traces(id) ON DELETE CASCADE,
parent_span_id UUID REFERENCES spans(id) ON DELETE SET NULL, -- arbre de spans
span_type TEXT NOT NULL CHECK (span_type IN (
'retrieval', 'embedding', 'reranking', 'llm_call',
'prompt_template', 'document_loader', 'custom'
)),
name TEXT NOT NULL, -- ex: "ChromaDB retrieval", "OpenAI embedding"
start_time TIMESTAMPTZ NOT NULL,
end_time TIMESTAMPTZ,
latency_ms INTEGER,
status TEXT DEFAULT 'success' CHECK (status IN ('success', 'error', 'skipped')),
error_message TEXT,
-- Données spécifiques au type (flexibilité maximale en JSONB)
input_data JSONB, -- requête/input de ce span
output_data JSONB, -- résultat de ce span
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_spans_trace_id ON spans(trace_id_fk);
CREATE INDEX idx_spans_type ON spans(span_type);
CREATE INDEX idx_spans_parent ON spans(parent_span_id) WHERE parent_span_id IS NOT NULL;
-- ============================================================
-- CHUNKS RÉCUPÉRÉS (détail du retrieval par trace)
-- ============================================================
CREATE TABLE retrieved_chunks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
trace_id_fk UUID NOT NULL REFERENCES traces(id) ON DELETE CASCADE,
span_id UUID REFERENCES spans(id) ON DELETE SET NULL,
rank_position INTEGER NOT NULL, -- position dans les résultats (1-based)
chunk_id_external TEXT, -- ID du chunk dans le vector store
content TEXT NOT NULL,
source_document TEXT, -- nom/URL du document source
source_page INTEGER,
similarity_score NUMERIC(6,5), -- score cosine (0 à 1)
rerank_score NUMERIC(6,5), -- score après reranking
is_used_in_prompt BOOLEAN DEFAULT FALSE, -- ce chunk a-t-il été inclus dans le prompt final ?
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_chunks_trace_id ON retrieved_chunks(trace_id_fk);
CREATE INDEX idx_chunks_rank ON retrieved_chunks(trace_id_fk, rank_position);
-- ============================================================
-- ÉVALUATIONS (annotations manuelles ou auto)
-- ============================================================
CREATE TABLE evaluations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
trace_id_fk UUID NOT NULL REFERENCES traces(id) ON DELETE CASCADE,
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
evaluator_type TEXT NOT NULL CHECK (evaluator_type IN ('human', 'llm_judge', 'rule_based')),
evaluator_id UUID REFERENCES profiles(id), -- NULL si auto
metric_name TEXT NOT NULL, -- ex: "faithfulness", "relevance", "custom_correctness"
score NUMERIC(4,3), -- 0 à 1
label TEXT, -- ex: "correct", "hallucinated", "partial"
reasoning TEXT, -- explication du score
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_evaluations_trace_id ON evaluations(trace_id_fk);
CREATE INDEX idx_evaluations_metric ON evaluations(metric_name);
CREATE INDEX idx_evaluations_org_id ON evaluations(organization_id);
-- ============================================================
-- ALERTES (règles configurées par les utilisateurs)
-- ============================================================
CREATE TABLE alert_rules (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
project_id UUID REFERENCES projects(id) ON DELETE CASCADE,
name TEXT NOT NULL,
is_active BOOLEAN DEFAULT TRUE,
metric TEXT NOT NULL, -- ex: "faithfulness_score", "total_latency_ms", "error_rate"
operator TEXT NOT NULL CHECK (operator IN ('lt', 'gt', 'lte', 'gte', 'eq')),
threshold NUMERIC NOT NULL,
window_minutes INTEGER DEFAULT 60, -- fenêtre d'évaluation
channel TEXT NOT NULL CHECK (channel IN ('email', 'slack', 'webhook')),
destination TEXT NOT NULL, -- email, webhook URL, slack channel
cooldown_minutes INTEGER DEFAULT 30, -- anti-spam
last_triggered_at TIMESTAMPTZ,
created_by UUID NOT NULL REFERENCES profiles(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_alert_rules_org_id ON alert_rules(organization_id);
CREATE INDEX idx_alert_rules_project_id ON alert_rules(project_id) WHERE project_id IS NOT NULL;
CREATE INDEX idx_alert_rules_active ON alert_rules(is_active) WHERE is_active = TRUE;
-- ============================================================
-- HISTORIQUE DES ALERTES DÉCLENCHÉES
-- ============================================================
CREATE TABLE alert_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
alert_rule_id UUID NOT NULL REFERENCES alert_rules(id) ON DELETE CASCADE,
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
triggered_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
metric_value NUMERIC NOT NULL,
threshold_value NUMERIC NOT NULL,
traces_affected INTEGER DEFAULT 0,
notification_sent BOOLEAN DEFAULT FALSE,
notification_error TEXT
);
CREATE INDEX idx_alert_events_rule_id ON alert_events(alert_rule_id);
CREATE INDEX idx_alert_events_triggered_at ON alert_events(triggered_at DESC);
-- ============================================================
-- USAGE QUOTAS (tracking pour les limites de plan)
-- ============================================================
CREATE TABLE usage_counters (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
period_start DATE NOT NULL, -- début du mois de facturation
traces_count INTEGER DEFAULT 0,
spans_count INTEGER DEFAULT 0,
api_calls_count INTEGER DEFAULT 0,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(organization_id, period_start)
);
CREATE INDEX idx_usage_org_period ON usage_counters(organization_id, period_start DESC);
RLS Policies
-- ============================================================-- Activer RLS sur toutes les tables
-- ============================================================
ALTER TABLE organizations ENABLE ROW LEVEL SECURITY;
ALTER TABLE organization_members ENABLE ROW LEVEL SECURITY;
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
ALTER TABLE api_keys ENABLE ROW LEVEL SECURITY;
ALTER TABLE traces ENABLE ROW LEVEL SECURITY;
ALTER TABLE spans ENABLE ROW LEVEL SECURITY;
ALTER TABLE retrieved_chunks ENABLE ROW LEVEL SECURITY;
ALTER TABLE evaluations ENABLE ROW LEVEL SECURITY;
ALTER TABLE alert_rules ENABLE ROW LEVEL SECURITY;
ALTER TABLE alert_events ENABLE ROW LEVEL SECURITY;
ALTER TABLE usage_counters ENABLE ROW LEVEL SECURITY;
-- ============================================================
-- Helper function : obtenir les org IDs accessibles pour un user
-- ============================================================
CREATE OR REPLACE FUNCTION get_user_org_ids(user_uuid UUID)
RETURNS UUID[] AS $$
SELECT ARRAY(
SELECT organization_id
FROM organization_members
WHERE user_id = user_uuid AND accepted_at IS NOT NULL
);
$$ LANGUAGE sql STABLE SECURITY DEFINER;
-- ============================================================
-- ORGANIZATIONS
-- ============================================================
CREATE POLICY "org_select" ON organizations
FOR SELECT USING (id = ANY(get_user_org_ids(auth.uid())));
CREATE POLICY "org_insert" ON organizations
FOR INSERT WITH CHECK (owner_id = auth.uid());
CREATE POLICY "org_update" ON organizations
FOR UPDATE USING (owner_id = auth.uid());
-- ============================================================
-- ORGANIZATION_MEMBERS
-- ============================================================
CREATE POLICY "members_select" ON organization_members
FOR SELECT USING (organization_id = ANY(get_user_org_ids(auth.uid())));
CREATE POLICY "members_insert_owner" ON organization_members
FOR INSERT WITH CHECK (
EXISTS (
SELECT 1 FROM organizations
WHERE id = organization_id AND owner_id = auth.uid()
)
);
CREATE POLICY "members_delete" ON organization_members
FOR DELETE USING (
user_id = auth.uid() OR
EXISTS (
SELECT 1 FROM organizations
WHERE id = organization_id AND owner_id = auth.uid()
)
);
-- ============================================================
-- PROJECTS
-- ============================================================
CREATE POLICY "projects_select" ON projects
FOR SELECT USING (organization_id = ANY(get_user_org_ids(auth.uid())));
CREATE POLICY "projects_insert" ON projects
FOR INSERT WITH CHECK (organization_id = ANY(get_user_org_ids(auth.uid())));
CREATE POLICY "projects_update" ON projects
FOR UPDATE USING (organization_id = ANY(get_user_org_ids(auth.uid())));
CREATE POLICY "projects_delete" ON projects
FOR DELETE USING (
EXISTS (
SELECT 1 FROM organizations
WHERE id = organization_id AND owner_id = auth.uid()
)
);
-- ============================================================
-- API_KEYS (lecture seulement via dashboard, écriture via service role)
-- ============================================================
CREATE POLICY "apikeys_select" ON api_keys
FOR SELECT USING (organization_id = ANY(get_user_org_ids(auth.uid())));
CREATE POLICY "apikeys_insert" ON api_keys
FOR INSERT WITH CHECK (organization_id = ANY(get_user_org_ids(auth.uid())));
CREATE POLICY "apikeys_update_revoke" ON api_keys
FOR UPDATE USING (organization_id = ANY(get_user_org_ids(auth.uid())));
-- ============================================================
-- TRACES (ingestion via service role, lecture via dashboard)
-- ============================================================
CREATE POLICY "traces_select" ON traces
FOR SELECT USING (organization_id = ANY(get_user_org_ids(auth.uid())));
-- INSERT/UPDATE via service_role uniquement (API d'ingestion)
-- ============================================================
-- SPANS
-- ============================================================
CREATE POLICY "spans_select" ON spans
FOR SELECT USING (
EXISTS (
SELECT 1 FROM traces t
WHERE t.id = trace_id_fk
AND t.organization_id = ANY(get_user_org_ids(auth.uid()))
)
);
-- ============================================================
-- RETRIEVED_CHUNKS
-- ============================================================
CREATE POLICY "chunks_select" ON retrieved_chunks
FOR SELECT USING (
EXISTS (
SELECT 1 FROM traces t
WHERE t.id = trace_id_fk
AND t.organization_id = ANY(get_user_org_ids(auth.uid()))
)
);
-- ============================================================
-- EVALUATIONS
-- ============================================================
CREATE POLICY "evals_select" ON evaluations
FOR SELECT USING (organization_id = ANY(get_user_org_ids(auth.uid())));
CREATE POLICY "evals_insert" ON evaluations
FOR INSERT WITH CHECK (organization_id = ANY(get_user_org_ids(auth.uid())));
-- ============================================================
-- ALERT_RULES
-- ============================================================
CREATE POLICY "alerts_all" ON alert_rules
FOR ALL USING (organization_id = ANY(get_user_org_ids(auth.uid())));
-- ============================================================
-- ALERT_EVENTS
-- ============================================================
CREATE POLICY "alert_events_select" ON alert_events
FOR SELECT USING (organization_id = ANY(get_user_org_ids(auth.uid())));
-- ============================================================
-- USAGE_COUNTERS
-- ============================================================
CREATE POLICY "usage_select" ON usage_counters
FOR SELECT USING (organization_id = ANY(get_user_org_ids(auth.uid())));
4. Routes API
Ingestion (authentification par clé API dans le header X-API-Key)
| Method | Route | Description | Auth |
|--------|-------|-------------|------|
| POST | /api/ingest/trace | Ingérer une trace complète (query + spans + chunks + métriques) | API Key |
| POST | /api/ingest/trace/batch | Ingérer un batch de traces (max 50) | API Key |
| POST | /api/ingest/span | Ajouter un span à une trace existante (streaming progressif) | API Key |
|