Spaces:
Sleeping
Sleeping
claude's playground
feat(agent verf): Add multi-provider LLM infrastructure with dual-mode support
0a6602d | # ============================================================================ | |
| # 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 | |
| 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 | |