# ============================================================================ # agent verf - LangGraph Verification Workflow # Version: 0.1.0 # Last Updated: 2026-01-13 # # This is the main workflow definition using LangGraph. # # Workflow: # START → extraction → triage → evidence → synthesizer → END # # The workflow is a linear pipeline in the MVP. Future versions may add: # - Conditional routing based on triage decision # - Parallel evidence gathering # - Human-in-the-loop checkpoints # - Retry/fallback branches # # Usage: # receipt = await verify_content(url="https://twitter.com/...") # ============================================================================ import structlog from typing import Optional, Any from uuid import UUID from datetime import datetime from langgraph.graph import StateGraph, END from src.graph.state import VerificationState, create_initial_state from src.utils.llm import VerificationMode from src.graph.nodes import ( triage_node, extraction_node, evidence_node, synthesizer_node, ) from src.models.receipt import VerificationReceipt, AgentSignature logger = structlog.get_logger() # ============================================================================ # Graph Definition # ============================================================================ def create_verification_graph() -> StateGraph: """ Create the verification workflow graph. Node order: 1. extraction - Fetch content from URL 2. triage - Analyze and route to lens 3. evidence - Gather sources 4. synthesizer - Generate verdict Returns: Compiled StateGraph ready for execution """ # Create the graph with our state schema workflow = StateGraph(VerificationState) # Add nodes workflow.add_node("extraction", extraction_node) workflow.add_node("triage", triage_node) workflow.add_node("evidence", evidence_node) workflow.add_node("synthesizer", synthesizer_node) # Define the flow (linear for MVP) workflow.set_entry_point("extraction") workflow.add_edge("extraction", "triage") workflow.add_edge("triage", "evidence") workflow.add_edge("evidence", "synthesizer") workflow.add_edge("synthesizer", END) # Compile the graph return workflow.compile() # ============================================================================ # Cached Graph Instance # # The graph is expensive to compile, so we cache it. # ============================================================================ _compiled_graph = None def get_compiled_graph(): """Get or create the compiled verification graph.""" global _compiled_graph if _compiled_graph is None: logger.info("Compiling verification graph") _compiled_graph = create_verification_graph() return _compiled_graph # ============================================================================ # Main Entry Point # ============================================================================ async def verify_content( url: Optional[str] = None, text: Optional[str] = None, user_id: Optional[UUID] = None, platform: str = "web", scan_mode: str = "quick_scan", mode: VerificationMode = VerificationMode.FREE, ) -> VerificationReceipt: """ Verify content and return a VerificationReceipt. This is the main entry point for the verification system. Args: url: URL to verify (e.g., tweet, video, article) text: Raw text to verify (if no URL) user_id: User who requested verification platform: Request source (web, telegram, discord) scan_mode: "quick_scan" (fast) or "deep_dive" (thorough) mode: LLM provider mode (FREE or VENICE) Modes: - FREE: Uses Groq, Google, Cloudflare for triage; Cerebras, OpenRouter for synthesis - VENICE: Uses Venice.ai first, falls back to free providers Returns: VerificationReceipt with verdict, evidence, and metadata Raises: ValueError: If neither url nor text is provided Example: receipt = await verify_content( url="https://twitter.com/someone/status/123456", user_id=user.id, platform="telegram", mode=VerificationMode.VENICE, ) print(receipt.verdict.status) # "FALSE" print(receipt.dm_response) # Formatted response for DM """ # Validate input if not url and not text: raise ValueError("Either url or text must be provided") logger.info( "Starting verification", url=url, has_text=text is not None, user_id=str(user_id) if user_id else None, platform=platform, scan_mode=scan_mode, mode=mode.value, ) # Create initial state with mode initial_state = create_initial_state( url=url, text=text, user_id=user_id, platform=platform, scan_mode=scan_mode, mode=mode, ) # Get compiled graph graph = get_compiled_graph() # Run the workflow try: # Execute the graph final_state = await graph.ainvoke(initial_state) # Convert to VerificationReceipt receipt = _state_to_receipt(final_state) logger.info( "Verification complete", request_id=str(receipt.id), verdict=receipt.verdict.status, confidence=receipt.verdict.confidence, sources_checked=receipt.sources_checked, cost_usd=receipt.signature.estimated_cost_usd, ) return receipt except Exception as e: logger.error( "Verification workflow failed", error=str(e), url=url, ) raise def _state_to_receipt(state: VerificationState) -> VerificationReceipt: """ Convert final VerificationState to a VerificationReceipt. The state contains all the workflow data. The receipt is the user-facing output. """ from src.models.content import Content, ContentType, Platform # Handle case where content extraction failed if state.content: content = state.content else: # Create minimal content content = Content( source_url=state.request_url, platform=Platform.WEB, content_type=ContentType.TEXT_POST, text_content=state.request_text or "", extracted_at=datetime.utcnow(), extraction_method="none", ) # Handle case where verdict wasn't generated if state.verdict: verdict = state.verdict else: from src.models.verdict import Verdict, VerdictStatus verdict = Verdict( status=VerdictStatus.UNVERIFIED, confidence=0.0, summary="Verification did not complete", detailed_reasoning="The workflow did not produce a verdict", key_findings=[], primary_lens="general", ) # Build agent signature signature = AgentSignature( model_name=state.meta_models_used[0] if state.meta_models_used else "unknown", model_version=None, tools_used=state.meta_tools_used, processing_time_ms=state.processing_time_ms, estimated_cost_usd=state.meta_total_cost_usd, system_version="0.1.0", ) # Create receipt receipt = VerificationReceipt( id=state.request_id, created_at=state.meta_completed_at or datetime.utcnow(), user_id=state.request_user_id, request_platform=state.request_platform, scan_mode=state.request_scan_mode, content=content, verdict=verdict, evidence=state.evidence, sources_checked=state.sources_checked, ai_detection=None, # Not implemented in MVP ipfs_hash=None, # Will be set after IPFS upload signature=signature, ) return receipt # ============================================================================ # Quick Verification (Simplified API) # ============================================================================ async def quick_verify( url: str, mode: VerificationMode = VerificationMode.FREE, ) -> dict[str, Any]: """ Quick verification returning just the essential info. This is a convenience wrapper for simple use cases. Args: url: URL to verify mode: LLM provider mode (FREE or VENICE) Returns: Dict with verdict, confidence, and summary Example: result = await quick_verify("https://twitter.com/...") print(result["verdict"]) # "FALSE" print(result["confidence"]) # 0.92 print(result["summary"]) # "This claim is false because..." """ receipt = await verify_content(url=url, mode=mode) return { "verdict": receipt.verdict.status, "confidence": receipt.verdict.confidence, "summary": receipt.verdict.summary, "report_url": receipt.report_url, "sources_checked": receipt.sources_checked, "mode": mode.value, }