agent-verif / packages /core /src /graph /workflow.py
claude's playground
feat(agent verf): Add multi-provider LLM infrastructure with dual-mode support
0a6602d
Raw
History Blame Contribute Delete
9.14 kB
# ============================================================================
# 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,
}