Orchestrator Design Overview
The system comprises 13 specialized agents organized into 5 functional layers, coordinated by a central LangGraph state machine. Each agent owns a narrow responsibility and communicates through a typed shared state.
● Why 13 Agents?
Each agent follows the Single Responsibility Principle. The PDF Ingestion Agent only extracts text and bounding boxes — it doesn't embed, it doesn't build triples. This means you can swap out the embedding model without touching ingestion logic, upgrade the SPARQL generator without affecting vector search, or add a new validation layer without rewriting synthesis. In a regulated domain like insurance, this separation also gives you auditability: you can trace exactly which agent produced which piece of the final answer.
● Why LangGraph over CrewAI?
LangGraph provides explicit state management through TypedDict, conditional routing via edges, and native support for parallel execution and human-in-the-loop checkpoints. For a financial services system where every state transition must be auditable and the orchestration logic must handle 5 distinct retrieval patterns with branching, LangGraph's graph-based state machine is the right choice. CrewAI YAML configs are also provided (Section 8) for teams preferring role-based setup.
Agent Registry — Complete Specifications
Each agent has a defined role, goal, backstory, tools, input/output schema, and the LangGraph node it occupies. Organized by the 5 functional layers.
LAYER 1 — INGESTION & INTAKE (Offline Pipeline)
PDF Ingestion Agent
Role: Extract raw text, tables, and bounding box coordinates from insurance PDFs using pdfplumber. Assign hierarchical document IDs ({CATEGORY}_{SUBCATEGORY}_{ORG}_{YEAR}_{SEQ}). Detect document type (policy, rider, endorsement, claim form).
Input: Raw PDF file path → Output: Structured pages with text + bbox metadata
Chunk & Embed Agent
Role: Split extracted pages into semantically coherent chunks (target: 512 tokens with 50-token overlap). Generate embeddings via text-embedding-3-large. Store each chunk with full metadata: chunk_id, pdf_id, page_number, chunk_sequence, bbox, embedding vector.
Input: Extracted pages → Output: Embedded chunks in Pinecone + metadata in PostgreSQL
TTL Generator Agent
Role: Convert chunks into FIBO-aligned RDF triples in Turtle format. Use ontology-guided NER to identify policy clauses, coverage terms, exclusions, waiting periods, and premium structures. Each triple carries prov:wasDerivedFrom linking back to the source chunk ID. Uses LLM-assisted triple extraction for complex clauses.
Input: Embedded chunks → Output: Individual .ttl files per PDF in GraphDB
Cluster Manager Agent
Role: Maintain semantic clusters of TTL files for efficient cross-policy SPARQL queries. Generate clustered TTL files by insurance type/provider/year. Track cluster membership and staleness in cluster_manifest.json. Trigger quarterly re-clustering via Louvain community detection on query co-occurrence graphs.
Input: Individual TTLs + query logs → Output: Cluster TTLs + manifest
LAYER 2 — QUERY UNDERSTANDING (Real-Time)
Query Classifier Agent
Role: Analyze incoming query using lightweight LLM (Claude Haiku) to determine: entity density, comparison indicators, reasoning depth (single vs multi-hop), precision requirements. Outputs a classification label matching one of the 5 retrieval patterns.
Input: Raw query string → Output: QueryClassification object
Entity Extractor Agent
Role: Extract named entities from the query — policy IDs, provider names, disease names, clause types, financial terms — and resolve them to ontology URIs. Uses both regex patterns (for known formats like LIFE_TERM_PRUDENTIAL_*) and LLM-based NER (for natural language entity mentions).
Input: Raw query → Output: List of EntityMention with ontology URIs
Route Planner Agent
Role: Based on the QueryClassification and extracted entities, select the retrieval pattern (1–5) and plan the execution sequence. For Pattern 3 (Vector→Graph), it specifies: "first search vectors for critical illness chunks in {providers}, then extract structured attributes via SPARQL." Generates an execution DAG.
Input: Classification + entities → Output: ExecutionPlan (ordered agent calls)
LAYER 3 — RETRIEVAL & SEARCH (Real-Time)
Vector Retriever Agent
Role: Execute semantic similarity search against Pinecone. Embed the query, retrieve top-k chunks (default k=20), apply cross-encoder re-ranking (ms-marco-MiniLM), deduplicate overlapping chunks, and return ranked results with full metadata (pdf_id, page_number, bbox).
Input: Query + filter params → Output: Ranked chunk list with scores
Graph Retriever Agent
Role: Generate and execute SPARQL queries against GraphDB. For structural queries, traverses from entity nodes to related clauses. For comparison queries, extracts structured attributes (coverage amounts, waiting periods, exclusion lists) into a tabular format. Respects cluster boundaries — queries individual TTLs for specific policies, cluster TTLs for cross-policy aggregation.
Input: Entities + query type → Output: Structured results + graph paths
PathRAG Traverser Agent
Role: Handle multi-hop reasoning queries (Pattern 5). Traces relationship paths through the knowledge graph: pre-existing condition → waiting period → subsequent condition → causal chain → coverage determination. At each hop, optionally invokes vector search for clause interpretation context. Uses Reciprocal Rank Fusion (RRF) to merge graph paths with vector results.
Input: Multi-hop query plan → Output: Fused path+vector results
LAYER 4 — REASONING & VALIDATION (Real-Time)
Hallucination Guard Agent
Role: Validate every factual claim in the draft answer against the retrieved context. For each claim, check: (1) Is it directly supported by at least one retrieved chunk? (2) Does the graph path confirm the relationship? (3) Is the numerical value (amounts, durations) exactly quoted? Flag unsupported claims for removal or re-retrieval. Enforces "no fact without a source" policy.
Input: Draft answer + context → Output: Validated answer with confidence scores
Contradiction Detector Agent
Role: Before synthesis, scan all retrieved chunks and graph results for contradictions. Run SPARQL contradiction-detection queries (same condition, different values). When contradictions are found, flag them explicitly in the answer ("Note: Policy X states 90 days while Policy Y states 60 days for the same condition") rather than letting the LLM silently average them.
Input: Retrieved results set → Output: Contradiction report + cleaned context
LAYER 5 — SYNTHESIS & DELIVERY (Real-Time)
Answer Synthesizer Agent
Role: Generate the final natural-language answer using Claude Sonnet/Opus. Receives validated context chunks, structured graph results, contradiction reports, and the original query. Produces an answer with inline chunk ID citations [C042]. For comparison queries, generates formatted tables. Follows a strict prompt template that demands per-fact attribution.
Input: Validated context + query → Output: Answer text with [chunk_id] citations
Provenance Citer Agent
Role: Parse chunk IDs from the synthesized answer, resolve each to its pdf_id + page_number + bbox coordinates via PostgreSQL metadata lookup. Generate the citation block ("Source: Prudential Term Life 2024, Pages 15, 27–29"). Package the highlight coordinates for the dual-pane PDF viewer UI. This is the final agent before response delivery.
Input: Answer with [C_ids] → Output: Final cited response + PDF highlight data
Tool Registry — Complete Specifications
Every tool follows the LangChain BaseTool interface with typed inputs/outputs, error handling, and retry logic. Tools are shared across agents where appropriate.
| # | Tool Name | Used By | Description | External Service |
|---|---|---|---|---|
| 1 | PDFExtractorTool | Agent 01 | Extract text + bounding boxes from PDF using pdfplumber. Returns page-by-page content with (x,y,w,h) coordinates for every text block. | pdfplumber lib |
| 2 | DocumentClassifierTool | Agent 01 | Classify document type (policy, rider, endorsement, claim form, brochure) using a fine-tuned classifier or LLM zero-shot. | Claude Haiku |
| 3 | IDGeneratorTool | Agent 01 | Generate hierarchical IDs: {CAT}_{SUBCAT}_{ORG}_{YEAR}_{SEQ}. Checks PostgreSQL for sequence uniqueness. | PostgreSQL |
| 4 | SemanticChunkerTool | Agent 02 | Split pages into semantically coherent chunks (512 tokens, 50-token overlap). Uses sentence-boundary detection and semantic similarity thresholds. | Local (spaCy) |
| 5 | EmbeddingTool | Agents 02, 08 | Generate 1536-dim embeddings via text-embedding-3-large. Batched processing for ingestion, single-query for real-time. | OpenAI API |
| 6 | PineconeUpsertTool | Agent 02 | Upsert vectors with metadata (pdf_id, page_number, chunk_sequence, category, provider) to Pinecone serverless index. | Pinecone |
| 7 | PostgresWriteTool | Agent 02 | Write chunk metadata (bbox coords, TTL URIs, cluster membership) to PostgreSQL with JSONB indexing. | PostgreSQL 15+ |
| 8 | OntologyMapperTool | Agent 03 | Map extracted entities to FIBO ontology classes. Uses a lookup table of 200+ insurance-specific concept → FIBO URI mappings. | Local ontology |
| 9 | TripleExtractorTool | Agent 03 | LLM-assisted RDF triple extraction from text chunks. Outputs subject-predicate-object triples with prov:wasDerivedFrom provenance. | Claude Sonnet |
| 10 | GraphDBLoaderTool | Agent 03 | Load TTL files into Ontotext GraphDB via SPARQL UPDATE. Creates named graphs per document. | GraphDB |
| 11 | LLMClassifierTool | Agent 05 | Lightweight LLM call (Haiku) to classify query intent: entity density, comparison indicators, reasoning depth, precision needs. | Claude Haiku |
| 12 | NERTool | Agent 06 | Named Entity Recognition combining regex patterns (policy IDs, providers) with LLM-based NER (diseases, clause types). | Local + LLM |
| 13 | PineconeSearchTool | Agent 08 | Execute cosine similarity search against Pinecone with metadata filters (provider, category, year range). Returns top-k with scores. | Pinecone |
| 14 | CrossEncoderTool | Agent 08 | Re-rank initial vector results using cross-encoder model (ms-marco-MiniLM-L-12-v2) for higher precision. | Local model |
| 15 | SPARQLGeneratorTool | Agents 09, 12 | Generate SPARQL 1.1 queries from natural language + entity URIs. Supports SELECT, CONSTRUCT, and ASK query forms. | LLM + templates |
| 16 | GraphDBQueryTool | Agents 09, 10 | Execute SPARQL queries against GraphDB endpoint. Handles named graph selection (individual vs cluster). | GraphDB |
| 17 | RRFMergerTool | Agent 10 | Reciprocal Rank Fusion to merge results from vector search and graph traversal into a unified ranked list. | Local |
| 18 | LLMSynthesisTool | Agent 13a | Claude Opus/Sonnet call with structured prompt template demanding per-fact [chunk_id] attribution. | Claude Sonnet |
Shared State Schema & Graph Definition
The entire orchestration is defined as a LangGraph StateGraph with a TypedDict shared state. Every agent reads from and writes to this state, and conditional edges route between agents based on the retrieval pattern selected.
● GraphState — TypedDict Definition
from typing import TypedDict, List, Optional, Literal from dataclasses import dataclass, field from enum import Enum class RetrievalPattern(Enum): VECTOR_ONLY = "vector_only" GRAPH_ONLY = "graph_only" VECTOR_THEN_GRAPH = "vector_then_graph" GRAPH_THEN_VECTOR = "graph_then_vector" PATHRAG_HYBRID = "pathrag_hybrid" class GraphState(TypedDict): # ── Layer 2: Query Understanding ── query: str # Original user query classification: dict # {entity_density, has_comparison, reasoning_hops, precision} entities: List[dict] # [{text, type, ontology_uri, confidence}] retrieval_pattern: RetrievalPattern # Selected pattern (1-5) execution_plan: List[str] # Ordered list of agent node names to invoke # ── Layer 3: Retrieval Results ── vector_results: List[dict] # [{chunk_id, text, score, pdf_id, page, bbox}] graph_results: List[dict] # [{subject, predicate, object, source_chunk}] pathrag_results: List[dict] # [{path, hops, vector_context, fused_score}] merged_context: List[dict] # RRF-merged final context # ── Layer 4: Validation ── contradictions: List[dict] # [{clause1, clause2, value1, value2, condition}] validated_context: List[dict] # Filtered context after hallucination guard confidence_scores: dict # Per-claim confidence # ── Layer 5: Synthesis ── draft_answer: str # LLM-generated answer with [C_id] citations final_answer: str # Validated answer after hallucination guard citations: List[dict] # [{chunk_id, pdf_id, page, bbox, text_snippet}] pdf_highlights: List[dict] # [{pdf_id, page, x, y, width, height, color}] # ── Metadata ── error: Optional[str] # Error message if any step fails iteration_count: int # Re-retrieval loop counter (max 2) human_review: bool # Flag for human-in-the-loop checkpoint
● LangGraph Builder — Complete Graph Definition
from langgraph.graph import StateGraph, END from langgraph.checkpoint.memory import MemorySaver def build_orchestrator() -> StateGraph: graph = StateGraph(GraphState) # ═══ Register all 13 agent nodes ═══ # Layer 2: Query Understanding graph.add_node("classify_query", query_classifier_agent) graph.add_node("extract_entities", entity_extractor_agent) graph.add_node("plan_route", route_planner_agent) # Layer 3: Retrieval graph.add_node("vector_retrieve", vector_retriever_agent) graph.add_node("graph_retrieve", graph_retriever_agent) graph.add_node("pathrag_traverse", pathrag_traverser_agent) # Layer 4: Validation graph.add_node("detect_contradictions", contradiction_detector_agent) graph.add_node("guard_hallucination", hallucination_guard_agent) # Layer 5: Synthesis graph.add_node("synthesize_answer", answer_synthesizer_agent) graph.add_node("cite_provenance", provenance_citer_agent) # ═══ Define edges ═══ # Sequential: classify → extract → plan → (conditional routing) graph.set_entry_point("classify_query") graph.add_edge("classify_query", "extract_entities") graph.add_edge("extract_entities", "plan_route") # ═══ Conditional routing based on retrieval pattern ═══ graph.add_conditional_edges( "plan_route", route_to_retriever, # Router function (below) { "vector_only": "vector_retrieve", "graph_only": "graph_retrieve", "vector_then_graph": "vector_retrieve", # Start with vector "graph_then_vector": "graph_retrieve", # Start with graph "pathrag_hybrid": "pathrag_traverse", } ) # ═══ Pattern-specific chaining ═══ # After vector retrieval, check if we need graph refinement graph.add_conditional_edges( "vector_retrieve", needs_graph_refinement, { "refine": "graph_retrieve", # Pattern 3: Vec→Graph "validate": "detect_contradictions", # Pattern 1: Vec only } ) # After graph retrieval, check if we need vector expansion graph.add_conditional_edges( "graph_retrieve", needs_vector_expansion, { "expand": "vector_retrieve", # Pattern 4: Graph→Vec "validate": "detect_contradictions", # Pattern 2 or 3 done } ) # PathRAG always goes to validation graph.add_edge("pathrag_traverse", "detect_contradictions") # Validation → Synthesis → Citation → END graph.add_edge("detect_contradictions", "guard_hallucination") # Hallucination guard can trigger re-retrieval (max 2 loops) graph.add_conditional_edges( "guard_hallucination", check_confidence, { "sufficient": "synthesize_answer", "re_retrieve": "plan_route", # Loop back with refined query "human_review": "synthesize_answer", # Flag for HITL } ) graph.add_edge("synthesize_answer", "cite_provenance") graph.add_edge("cite_provenance", END) # ═══ Compile with checkpointing ═══ memory = MemorySaver() return graph.compile(checkpointer=memory) # ═══ Router Functions ═══ def route_to_retriever(state: GraphState) -> str: return state["retrieval_pattern"].value def needs_graph_refinement(state: GraphState) -> str: if state["retrieval_pattern"] == RetrievalPattern.VECTOR_THEN_GRAPH: return "refine" return "validate" def needs_vector_expansion(state: GraphState) -> str: if state["retrieval_pattern"] == RetrievalPattern.GRAPH_THEN_VECTOR: return "expand" return "validate" def check_confidence(state: GraphState) -> str: if state["iteration_count"] >= 2: return "human_review" # Max retries exceeded avg_conf = sum(state["confidence_scores"].values()) / max(len(state["confidence_scores"]), 1) if avg_conf >= 0.8: return "sufficient" return "re_retrieve"
How Agents Collaborate Per Pattern
Each of the 5 retrieval patterns activates a different subset and ordering of agents. Here's the exact execution sequence for each.
| Pattern | Trigger | Agent Execution Sequence | Example Query |
|---|---|---|---|
| P1: Vector-Only | Fuzzy / exploratory, no specific entities | Classifier → Entity Extractor → Route Planner → Vector Retriever → Contradiction Detector → Hallucination Guard → Synthesizer → Provenance Citer | "What policies cover chronic kidney disease?" |
| P2: Graph-Only | Structural, known entities, precise | Classifier → Entity Extractor → Route Planner → Graph Retriever → Contradiction Detector → Hallucination Guard → Synthesizer → Provenance Citer | "List all exclusions in LIFE_TERM_PRUDENTIAL_2024_00127" |
| P3: Vec → Graph | Comparison queries | Classifier → Entity Extractor → Route Planner → Vector Retriever → Graph Retriever → Contradiction Detector → Hallucination Guard → Synthesizer → Provenance Citer | "Compare CI coverage across Prudential, HDFC, LIC" |
| P4: Graph → Vec | Claim validation | Classifier → Entity Extractor → Route Planner → Graph Retriever → Vector Retriever → Contradiction Detector → Hallucination Guard → Synthesizer → Provenance Citer | "Can this diabetes claim be approved under policy X?" |
| P5: PathRAG | Multi-hop reasoning, causal chains | Classifier → Entity Extractor → Route Planner → PathRAG Traverser (graph hops + vector at each hop) → Contradiction Detector → Hallucination Guard → Synthesizer → Provenance Citer | "Pre-existing hypertension → diabetes → heart attack claim?" |
● Re-Retrieval Loop (Hallucination Guard Feedback)
When the Hallucination Guard finds claims with confidence below 0.8, it triggers a re-retrieval loop. The state's iteration_count increments, the Route Planner is re-invoked with a refined query (adding specificity based on what was missing), and the retrieval agents run again with adjusted parameters (higher k, different filters). Maximum 2 loops before escalating to human review. This closed-loop design ensures answer quality meets financial services regulatory standards.
Multi-Agent Flow Simulator
Watch all 13 agents collaborate in real time. Select a query to see which agents activate, in what order, and what each contributes to the final answer.
▶ Agent Execution Trace
InteractiveProject Structure & Setup
Complete production project layout with all agent implementations, tool definitions, and configuration.
● Project Directory
insurance_kg_rag/ ├── agents/ │ ├── __init__.py │ ├── layer1_ingestion/ │ │ ├── pdf_ingestion_agent.py # Agent 01 │ │ ├── chunk_embed_agent.py # Agent 02 │ │ ├── ttl_generator_agent.py # Agent 03 │ │ └── cluster_manager_agent.py # Agent 04 │ ├── layer2_understanding/ │ │ ├── query_classifier_agent.py # Agent 05 │ │ ├── entity_extractor_agent.py # Agent 06 │ │ └── route_planner_agent.py # Agent 07 │ ├── layer3_retrieval/ │ │ ├── vector_retriever_agent.py # Agent 08 │ │ ├── graph_retriever_agent.py # Agent 09 │ │ └── pathrag_traverser_agent.py # Agent 10 │ ├── layer4_validation/ │ │ ├── hallucination_guard_agent.py # Agent 11 │ │ └── contradiction_detector_agent.py # Agent 12 │ └── layer5_synthesis/ │ ├── answer_synthesizer_agent.py # Agent 13a │ └── provenance_citer_agent.py # Agent 13b ├── tools/ │ ├── __init__.py │ ├── pdf_tools.py # Tools 1-3 │ ├── embedding_tools.py # Tools 4-7 │ ├── ontology_tools.py # Tools 8-10 │ ├── classification_tools.py # Tools 11-12 │ ├── search_tools.py # Tools 13-14 │ ├── graph_tools.py # Tools 15-16 │ ├── fusion_tools.py # Tool 17 │ └── synthesis_tools.py # Tool 18 ├── state/ │ ├── __init__.py │ └── graph_state.py # GraphState TypedDict ├── orchestrator/ │ ├── __init__.py │ ├── graph_builder.py # LangGraph StateGraph definition │ └── router_functions.py # Conditional edge functions ├── ontology/ │ ├── insurance_fibo.ttl # FIBO-aligned insurance ontology │ ├── namespaces.py # URI namespace bindings │ └── concept_mappings.json # 200+ concept → FIBO URI mappings ├── config/ │ ├── config.yaml # All service endpoints, model names │ └── .env # API keys (Pinecone, OpenAI, Anthropic) ├── api/ │ └── server.py # FastAPI serving layer ├── ui/ │ └── dual_pane_viewer.html # Bloomberg-style answer + PDF viewer ├── crewai_config/ # Alternative CrewAI YAML configs │ ├── agents.yaml │ └── tasks.yaml ├── docker-compose.yml ├── requirements.txt ├── main.py # Entry point └── README.md
● requirements.txt
# Core Orchestration langgraph==0.2.60 langchain==0.3.12 langchain-anthropic==0.3.5 langchain-openai==0.2.14 # Vector Store pinecone-client==5.0.1 # Graph Store SPARQLWrapper==2.0.0 rdflib==7.1.1 # Document Processing pdfplumber==0.11.4 spacy==3.8.3 # Embeddings & Re-ranking sentence-transformers==3.3.1 openai==1.58.1 # Database psycopg2-binary==2.9.10 redis==5.2.1 sqlalchemy==2.0.36 # API Server fastapi==0.115.6 uvicorn==0.34.0 # Utilities pydantic==2.10.3 python-dotenv==1.0.1 httpx==0.28.1
Alternative: CrewAI Role-Based Configuration
For teams preferring CrewAI's declarative YAML approach over LangGraph's programmatic graph definition. These configs mirror the same 13 agents and their tasks.
● agents.yaml (excerpt — 5 of 13)
query_classifier: role: Insurance Query Classifier goal: > Classify user queries by intent, entity density, comparison indicators, and reasoning depth to select the optimal retrieval pattern (1-5). backstory: > Expert in insurance domain query understanding with deep knowledge of policy terminology, claim workflows, and regulatory language. tools: - LLMClassifierTool - QueryExampleDBTool llm: claude-haiku-4-5-20251001 verbose: true entity_extractor: role: Insurance Entity Extractor goal: > Extract policy IDs, provider names, disease names, clause types, and financial terms from queries and resolve them to FIBO ontology URIs. backstory: > NER specialist trained on 100K+ insurance documents with expertise in FIBO, ICD-10, and insurance regulatory terminology. tools: - NERTool - OntologyResolverTool vector_retriever: role: Semantic Search Specialist goal: > Execute cosine similarity search, re-rank results, deduplicate overlapping chunks, and return the top-k most relevant context with full provenance. backstory: > Search engineer specializing in dense retrieval with cross-encoder re-ranking for financial docs. tools: - PineconeSearchTool - CrossEncoderTool - EmbeddingTool hallucination_guard: role: Financial Accuracy Validator goal: > Verify every factual claim against retrieved context. No fact without a source. Flag unsupported claims for removal or re-retrieval. backstory: > Regulatory compliance auditor who has reviewed thousands of insurance documents and knows that in financial services, accuracy is non-negotiable. tools: - ClaimVerifierTool - GraphPathValidatorTool answer_synthesizer: role: Insurance Answer Synthesizer goal: > Generate clear, accurate, fully-cited answers from validated context. Every fact must include a [C_id] citation. Comparison queries produce formatted tables. backstory: > Senior insurance analyst who can translate complex policy language into clear, actionable answers while maintaining regulatory-grade attribution. tools: - LLMSynthesisTool - ComparisonFormatterTool llm: claude-sonnet-4-20250514
● tasks.yaml (excerpt)
classify_query_task: description: > Analyze the query: "{query}" Determine entity density (count), comparison indicators, reasoning hops (single/multi), and precision requirements. Output the RetrievalPattern (1-5). expected_output: > JSON: {pattern, entity_density, has_comparison, reasoning_hops, precision_needed, confidence} agent: query_classifier extract_entities_task: description: > From query "{query}", extract all named entities: policy IDs, providers, diseases, clause types. Resolve each to its FIBO ontology URI. expected_output: > List of {text, type, ontology_uri, confidence} agent: entity_extractor context: - classify_query_task vector_search_task: description: > Search Pinecone for top-20 chunks matching the query. Apply filters from extracted entities. Re-rank with cross-encoder. Return with full metadata (pdf_id, page, bbox). expected_output: > Ranked list of {chunk_id, text, score, pdf_id, page_number, bbox} agent: vector_retriever context: - extract_entities_task validate_answer_task: description: > Verify every claim in the draft answer against retrieved context. For each claim: check chunk support, graph path confirmation, and numerical accuracy. Output confidence scores. expected_output: > Validated answer with per-claim confidence scores. Unsupported claims flagged for removal. agent: hallucination_guard context: - synthesize_answer_task cite_provenance_task: description: > Parse [C_id] citations from the validated answer. Resolve each chunk_id to pdf_id, page_number, and bbox coordinates. Generate citation block and PDF highlight data for dual-pane viewer. expected_output: > Final answer with human-readable citations and highlight coordinates for PDF viewer overlay. agent: provenance_citer context: - validate_answer_task