ReportEngine Architecture and Report Generation Pipeline: How the Bettafish Codebase Converts Agent Outputs into Structured Reports
The ReportEngine in the 666ghj/bettafish repository implements a modular, four-layer pipeline that transforms raw outputs from Query, Media, and Insight agents into polished HTML, Markdown, or PDF documents through orchestrated LLM-driven processing nodes and intermediate representation stitching.
The ReportEngine serves as the central reporting service within the Bettafish project, designed to synthesize heterogeneous data sources into publication-ready documents. Its architecture separates concerns between orchestration, processing, utilities, and rendering to maintain stateless, retry-safe operation while supporting real-time streaming feedback.
Four-Layer Architecture Overview
The ReportEngine architecture divides functionality into four logical layers that process data sequentially from raw inputs to final output files.
Orchestration Layer
The ReportAgent class in ReportEngine/agent.py acts as the central coordinator, maintaining workflow state and driving each processing stage. It manages the end-to-end lifecycle from input normalization through final persistence.
Processing Nodes
Individual LLM-driven steps handle specific transformation tasks:
TemplateSelectionNode→ Selects appropriate Markdown skeletonsDocumentLayoutNode→ Designs document-wide structure and themesWordBudgetNode→ Calculates per-chapter word targetsChapterGenerationNode→ Synthesizes content via LLM callsGraphRAGQueryNode→ Enhances chapters with knowledge graph context
Core Utilities
Helper modules in ReportEngine/core/ and ReportEngine/utils/ manage template parsing, intermediate representation (IR) stitching, chapter storage, configuration loading, and JSON validation.
Renderers and State
The rendering layer in ReportEngine/renderers/ converts IR into target formats (HTML, Markdown, PDF), while ReportEngine/state/state.py and ReportEngine/flask_interface.py provide runtime state management and HTTP API access.
Orchestration Layer: The ReportAgent Workflow
The ReportAgent class implements the primary generate_report() method, which executes an 11-step pipeline to convert agent outputs into structured documents.
Input Normalization and Template Selection
First, _normalize_reports() converts the three agent outputs (Query, Media, Insight) plus optional forum logs into plain strings. Then _select_template() invokes TemplateSelectionNode to analyze the query and content, selecting an appropriate Markdown template from the local repository.
Template Parsing and Layout Design
The _slice_template() method calls parse_template_sections from ReportEngine/core/template_parser.py to decompose the template into TemplateSection objects, each assigned a stable chapter_id. Subsequently, DocumentLayoutNode generates the document-wide design including titles, hero images, table of contents entries, and theme tokens.
Budget Planning and Context Building
WordBudgetNode computes per-chapter word targets and global writing guidelines. The agent then aggregates all inputs via _build_generation_context(), creating a single dictionary containing the query, normalized reports, layout specifications, and budget constraints for downstream consumption.
Optional GraphRAG Enhancement
When GRAPHRAG_ENABLED is true, _build_knowledge_graph() constructs a graph from the engine states and forum logs. During chapter generation, GraphRAGQueryNode executes multi-turn queries against this graph to enrich content with contextual relationships.
Chapter Generation and IR Stitching
For each TemplateSection, the agent invokes ChapterGenerationNode.run(), which may retry on JSON parsing errors or content-filter failures using fallback LLM chains (json_rescue_clients). Streaming callbacks (stream_handler) emit real-time progress events to UI clients.
After all chapters complete, DocumentComposer.build_document() in ReportEngine/core/stitcher.py performs IR stitching: sorting chapters, injecting unique anchors, merging metadata, and versioning the document structure.
Rendering and Persistence
Finally, HTMLRenderer (or MarkdownRenderer) consumes the IR via self.renderer.render(document_ir) to produce the output string. If save_report=True, _save_report() writes the HTML, IR JSON, and status files to self.config.OUTPUT_DIR/<report_id>/.
Processing Nodes: LLM-Driven Pipeline Steps
Each node inherits from BaseNode in ReportEngine/nodes/base_node.py and implements specific transformation logic.
TemplateSelectionNode
Located in ReportEngine/nodes/template_selection_node.py, this node calls the LLM with the query, engine reports, forum logs, and available templates. It returns a dictionary containing template_name, template_content, and selection_reason.
DocumentLayoutNode
This node in ReportEngine/nodes/document_layout_node.py generates the high-level document structure, determining title placement, hero imagery, navigation structure, and color theming.
WordBudgetNode
Implemented in ReportEngine/nodes/word_budget_node.py, this component analyzes template complexity and content depth to allocate word counts per chapter while establishing global writing constraints.
ChapterGenerationNode
Theheavy lifting occurs in ReportEngine/nodes/chapter_generation_node.py, which takes a TemplateSection and generation context to produce chapter JSON. It utilizes utils/json_parser.RobustJSONParser to handle malformed LLM outputs and supports configurable retry logic.
GraphRAGQueryNode
When enabled, ReportEngine/nodes/graphrag_query_node.py queries the knowledge graph built from agent states, returning graph_results and graph_enhancement_prompt to augment chapter content with relationship-aware context.
Core Utilities and IR Management
The utility layer ensures data integrity and transformation consistency throughout the pipeline.
Template Parsing
ReportEngine/core/template_parser.py defines the TemplateSection dataclass and parse_template_sections() function, which extracts hierarchical section numbers, URL slugs, and content outlines from Markdown templates.
IR Stitching
ReportEngine/core/stitcher.py contains DocumentComposer, which guarantees unique HTML anchors, assigns default chapterId values, and creates the final IR version stamped with IR_VERSION constants.
Chapter Storage and Validation
ReportEngine/core/chapter_storage.py creates per-run directories for debugging intermediate outputs. Supporting validation occurs in ReportEngine/utils/chart_validator.py and chart_review_service.py, which verify chart block well-formedness before rendering.
Configuration
ReportEngine/utils/config.py centralizes settings including API keys, model names, output directories, and feature toggles like GRAPHRAG_ENABLED.
Rendering Pipeline
The renderer layer converts the intermediate representation into human-readable formats.
HTMLRenderer
ReportEngine/renderers/html_renderer.py transforms IR into complete HTML pages, injecting CSS assets, anchor links, and dynamic table of contents navigation.
MarkdownRenderer
ReportEngine/renderers/markdown_renderer.py provides a fallback conversion that degrades unsupported block types (charts, word-clouds, widgets) into tables or textual placeholders, ensuring content accessibility even when advanced visualizations cannot render.
PDFRenderer
ReportEngine/renderers/pdf_renderer.py generates PDF documents by first rendering to HTML, then using weasyprint or headless browser engines to produce the final binary output.
State Management and Flask API
ReportEngine/state/state.py maintains the ReportState object, tracking metadata, generation timestamps, success/failure flags, and HTML content throughout the lifecycle.
The ReportEngine/flask_interface.py exposes REST endpoints including /run to initiate generation and /status/<task_id> for polling progress, enabling integration with external microservices and frontend applications.
End-to-End Workflow Example
Basic Synchronous Usage
from ReportEngine import ReportAgent
from config import settings
# Initialise the agent
agent = ReportAgent()
# Inputs from the three sub-engines
query_output = "### Query engine summary …"
media_output = "### Media analysis …"
insight_output = "### Insight findings …"
forum_logs = "User A: …\nUser B: …"
# Generate report
result = agent.generate_report(
query="武汉大学舆情分析",
reports=[query_output, media_output, insight_output],
forum_logs=forum_logs,
custom_template="", # Auto-select template
save_report=True,
)
print("HTML length:", len(result["html_content"]))
print("Files:", result.get("html_path"), result.get("ir_path"))
Streaming Progress Events
def stream_handler(event_type, payload):
# Forward to WebSocket or SSE client
print(f"[{event_type}] {payload}")
html_result = agent.generate_report(
query="武汉大学舆情分析",
reports=[q, m, i],
forum_logs=logs,
stream_handler=stream_handler,
save_report=False,
)
The stream_handler receives stage, progress, chapter_chunk, and error events emitted during the generation loop.
Disabling GraphRAG for Lightweight Runs
# Override at runtime
agent.config.GRAPHRAG_ENABLED = False
report = agent.generate_report(
query="疫情舆情报告",
reports=[q, m, i],
forum_logs=logs,
save_report=True,
)
When disabled, the engine skips graph building (lines ~600-630 of agent.py) and proceeds directly to chapter generation.
Summary
- The ReportEngine architecture comprises four layers: Orchestration (ReportAgent), Processing Nodes (LLM-driven transformers), Core Utilities (IR management), and Renderers (format converters).
- The pipeline is stateless between runs, persisting all intermediate data to per-run directories under
OUTPUT_DIR/<report_id>/. - ReportAgent.generate_report() implements an 11-step workflow from input normalization through template selection, chapter generation, IR stitching, and final rendering.
- Processing nodes inherit from BaseNode and handle specific tasks like template selection, layout design, word budgeting, and GraphRAG enhancement.
- IR stitching via DocumentComposer guarantees unique anchors and versioned metadata before HTML/Markdown rendering.
- Flask interface endpoints (
/run,/status/<task_id>) enable asynchronous integration with external services.
Frequently Asked Questions
How does ReportEngine handle malformed LLM outputs during chapter generation?
The ChapterGenerationNode in ReportEngine/nodes/chapter_generation_node.py implements retry logic wrapped around utils/json_parser.RobustJSONParser. When JSON parsing fails, content filters trigger, or outputs are too sparse, the node automatically attempts regeneration using fallback LLM clients (json_rescue_clients) to ensure valid chapter structure without manual intervention.
What file formats can the ReportEngine generate?
According to the renderer implementations in ReportEngine/renderers/, the engine supports HTML via HTMLRenderer, Markdown via MarkdownRenderer, and PDF via PDFRenderer (which uses weasyprint or headless browser conversion). The Markdown renderer serves as a robust fallback that converts unsupported visual elements like charts into textual tables.
Is the ReportEngine architecture stateless, and where are outputs stored?
Yes, the architecture is designed to be stateless between runs. All intermediate chapter JSON files, final HTML, IR documents, and status logs are written to a unique per-run directory structure under self.config.OUTPUT_DIR/<report_id>/ as implemented in ReportEngine/core/chapter_storage.py and the _save_report() method of ReportAgent.
How does the ReportEngine integrate with the three analysis agents (Query, Media, Insight)?
The engine receives raw string outputs from these agents via the reports parameter of generate_report(), which expects a list containing the Query, Media, and Insight outputs in sequence. The _normalize_reports() method standardizes these inputs before the TemplateSelectionNode and subsequent processing nodes synthesize them into coherent chapters, optionally enhanced by GraphRAGQueryNode when knowledge graph features are enabled.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →