agent-verif / packages /core /src /utils /supabase.py
claude's playground
feat(agent verf): Add multi-provider LLM infrastructure with dual-mode support
0a6602d
Raw
History Blame Contribute Delete
13.4 kB
# ============================================================================
# agent verf - Supabase Client
# Version: 0.1.0
# Last Updated: 2026-01-19
#
# Database client for Supabase (PostgreSQL + real-time + auth).
#
# Purpose:
# - Store and retrieve verification receipts
# - Store evidence linked to receipts
# - Query recent verifications for the feed
# - Handle all database operations with proper error handling
#
# Usage:
# client = get_supabase_client()
# receipt = await client.store_receipt(receipt_data)
# receipt = await client.get_receipt(receipt_id)
#
# Setup:
# 1. Create Supabase project at https://supabase.com
# 2. Run supabase_migration.sql in SQL Editor
# 3. Copy URL and keys to .env.local
# ============================================================================
import os
import structlog
from datetime import datetime
from typing import Optional, List
from uuid import UUID
from supabase import create_client, Client
from pydantic import BaseModel
logger = structlog.get_logger()
# ============================================================================
# Data Models (matching database schema)
# ============================================================================
class ReceiptInsert(BaseModel):
"""Data for inserting a receipt into the database."""
id: Optional[UUID] = None
source_url: Optional[str] = None
source_platform: Optional[str] = None
content_type: Optional[str] = None
content_text: Optional[str] = None
verdict_status: str
verdict_confidence: Optional[float] = None
verdict_summary: Optional[str] = None
verdict_reasoning: Optional[str] = None
key_findings: Optional[list] = None
primary_lens: Optional[str] = None
sources_checked: int = 0
processing_time_ms: Optional[int] = None
model_used: Optional[str] = None
tools_used: Optional[List[str]] = None
cost_usd: float = 0.0
request_platform: Optional[str] = None
scan_mode: str = "quick_scan"
class ReceiptRow(BaseModel):
"""Receipt row from the database."""
id: UUID
created_at: datetime
source_url: Optional[str]
source_platform: Optional[str]
content_type: Optional[str]
content_text: Optional[str]
verdict_status: str
verdict_confidence: Optional[float]
verdict_summary: Optional[str]
verdict_reasoning: Optional[str]
key_findings: Optional[list]
primary_lens: Optional[str]
sources_checked: int
processing_time_ms: Optional[int]
model_used: Optional[str]
tools_used: Optional[List[str]]
cost_usd: float
request_platform: Optional[str]
scan_mode: str
class EvidenceInsert(BaseModel):
"""Data for inserting evidence into the database."""
receipt_id: UUID
claim: Optional[str] = None
finding: Optional[str] = None
supports_verdict: Optional[bool] = None
source_url: Optional[str] = None
source_domain: Optional[str] = None
source_title: Optional[str] = None
authority_score: Optional[float] = None
class EvidenceRow(BaseModel):
"""Evidence row from the database."""
id: UUID
receipt_id: UUID
claim: Optional[str]
finding: Optional[str]
supports_verdict: Optional[bool]
source_url: Optional[str]
source_domain: Optional[str]
source_title: Optional[str]
authority_score: Optional[float]
created_at: datetime
# ============================================================================
# Supabase Client
# ============================================================================
class SupabaseClient:
"""
Client for Supabase database operations.
Handles all database interactions for receipts and evidence.
Uses the service role key for server-side operations.
Example:
client = SupabaseClient()
# Store a receipt
receipt = await client.store_receipt(ReceiptInsert(
verdict_status="TRUE",
verdict_confidence=0.95,
verdict_summary="This claim is accurate.",
))
# Get a receipt
receipt = await client.get_receipt(receipt_id)
# Get recent verifications
recent = await client.get_recent_verifications(limit=10)
"""
def __init__(self):
"""Initialize Supabase client from environment variables."""
self._url = os.getenv("SUPABASE_URL")
self._service_key = os.getenv("SUPABASE_SERVICE_KEY")
self._anon_key = os.getenv("SUPABASE_ANON_KEY")
if not self._url:
logger.warning("SUPABASE_URL not set - database operations will fail")
self._client = None
return
# Use service key for server-side operations (bypasses RLS)
key = self._service_key or self._anon_key
if not key:
logger.warning("No Supabase key set - database operations will fail")
self._client = None
return
try:
self._client: Client = create_client(self._url, key)
logger.info(
"Supabase client initialized",
url=self._url[:30] + "...",
using_service_key=bool(self._service_key),
)
except Exception as e:
logger.error("Failed to initialize Supabase client", error=str(e))
self._client = None
@property
def is_configured(self) -> bool:
"""Check if Supabase is properly configured."""
return self._client is not None
# -------------------------------------------------------------------------
# Receipt Operations
# -------------------------------------------------------------------------
async def store_receipt(self, data: ReceiptInsert) -> Optional[ReceiptRow]:
"""
Store a verification receipt in the database.
Args:
data: Receipt data to store
Returns:
The stored receipt with generated ID and timestamp, or None on failure
"""
if not self._client:
logger.error("Cannot store receipt - Supabase not configured")
return None
try:
# Convert to dict, excluding None values for optional fields
insert_data = data.model_dump(exclude_none=True)
# Insert and return the created row
result = self._client.table("receipts").insert(insert_data).execute()
if result.data and len(result.data) > 0:
row = ReceiptRow(**result.data[0])
logger.info(
"Receipt stored",
receipt_id=str(row.id),
verdict=row.verdict_status,
)
return row
else:
logger.error("Receipt insert returned no data")
return None
except Exception as e:
logger.error("Failed to store receipt", error=str(e))
return None
async def get_receipt(self, receipt_id: UUID) -> Optional[ReceiptRow]:
"""
Get a receipt by ID.
Args:
receipt_id: The receipt UUID
Returns:
The receipt if found, None otherwise
"""
if not self._client:
logger.error("Cannot get receipt - Supabase not configured")
return None
try:
result = (
self._client.table("receipts")
.select("*")
.eq("id", str(receipt_id))
.execute()
)
if result.data and len(result.data) > 0:
return ReceiptRow(**result.data[0])
return None
except Exception as e:
logger.error("Failed to get receipt", receipt_id=str(receipt_id), error=str(e))
return None
async def get_recent_verifications(
self,
limit: int = 20,
offset: int = 0,
) -> List[ReceiptRow]:
"""
Get recent verification receipts.
Args:
limit: Maximum number of receipts to return
offset: Number of receipts to skip (for pagination)
Returns:
List of receipts ordered by creation time (newest first)
"""
if not self._client:
logger.error("Cannot get recent verifications - Supabase not configured")
return []
try:
result = (
self._client.table("receipts")
.select("*")
.order("created_at", desc=True)
.range(offset, offset + limit - 1)
.execute()
)
return [ReceiptRow(**row) for row in result.data]
except Exception as e:
logger.error("Failed to get recent verifications", error=str(e))
return []
async def count_receipts(self) -> int:
"""Get total number of receipts."""
if not self._client:
return 0
try:
result = (
self._client.table("receipts")
.select("id", count="exact")
.execute()
)
return result.count or 0
except Exception as e:
logger.error("Failed to count receipts", error=str(e))
return 0
# -------------------------------------------------------------------------
# Evidence Operations
# -------------------------------------------------------------------------
async def store_evidence(self, data: EvidenceInsert) -> Optional[EvidenceRow]:
"""
Store evidence linked to a receipt.
Args:
data: Evidence data to store
Returns:
The stored evidence with generated ID, or None on failure
"""
if not self._client:
logger.error("Cannot store evidence - Supabase not configured")
return None
try:
insert_data = data.model_dump(exclude_none=True)
result = self._client.table("evidence").insert(insert_data).execute()
if result.data and len(result.data) > 0:
return EvidenceRow(**result.data[0])
return None
except Exception as e:
logger.error(
"Failed to store evidence",
receipt_id=str(data.receipt_id),
error=str(e),
)
return None
async def store_evidence_batch(
self,
evidence_list: List[EvidenceInsert],
) -> List[EvidenceRow]:
"""
Store multiple evidence items in a single batch.
Args:
evidence_list: List of evidence items to store
Returns:
List of stored evidence items
"""
if not self._client:
logger.error("Cannot store evidence batch - Supabase not configured")
return []
if not evidence_list:
return []
try:
insert_data = [e.model_dump(exclude_none=True) for e in evidence_list]
result = self._client.table("evidence").insert(insert_data).execute()
return [EvidenceRow(**row) for row in result.data]
except Exception as e:
logger.error("Failed to store evidence batch", error=str(e))
return []
async def get_evidence_for_receipt(self, receipt_id: UUID) -> List[EvidenceRow]:
"""
Get all evidence linked to a receipt.
Args:
receipt_id: The receipt UUID
Returns:
List of evidence items
"""
if not self._client:
return []
try:
result = (
self._client.table("evidence")
.select("*")
.eq("receipt_id", str(receipt_id))
.execute()
)
return [EvidenceRow(**row) for row in result.data]
except Exception as e:
logger.error(
"Failed to get evidence",
receipt_id=str(receipt_id),
error=str(e),
)
return []
# -------------------------------------------------------------------------
# Combined Operations
# -------------------------------------------------------------------------
async def get_receipt_with_evidence(
self,
receipt_id: UUID,
) -> Optional[tuple[ReceiptRow, List[EvidenceRow]]]:
"""
Get a receipt with all its evidence.
Args:
receipt_id: The receipt UUID
Returns:
Tuple of (receipt, evidence_list) or None if not found
"""
receipt = await self.get_receipt(receipt_id)
if not receipt:
return None
evidence = await self.get_evidence_for_receipt(receipt_id)
return (receipt, evidence)
# ============================================================================
# Singleton Instance
# ============================================================================
_supabase_client: Optional[SupabaseClient] = None
def get_supabase_client() -> SupabaseClient:
"""
Get the singleton Supabase client instance.
Usage:
client = get_supabase_client()
receipt = await client.store_receipt(...)
"""
global _supabase_client
if _supabase_client is None:
_supabase_client = SupabaseClient()
return _supabase_client