3. Epic News Architectural Patterns¶
This document describes key architectural patterns and solutions implemented in the Epic News system, with a strong focus on ensuring reliable, maintainable, and high-quality outputs.
1. Report Generation (DOCX)¶
Reports are DOCX files and nothing else (ADR-017). The core principle is to separate content generation from presentation: the crew produces structured data, and an assembler turns it into a document.
1.1. Assemblers¶
Pattern:
- Execute Crew: The CrewAI flow runs as usual to generate the core content. The
.kickoff()method returns aCrewOutputobject. - Parse to Pydantic Model: The raw output from the crew is parsed into a structured Pydantic model (e.g.,
SaintData,FinancialReport). - Assemble the DOCX: The model goes to the crew's assembler (
utils/docx_report/crews/). Some sections are written by one LLM call each, the others are filled in deterministically.build_docx()converts the Markdown fragments with pandoc, using the styles inreference.docx, and refuses any path outsideoutput/. - Record the path:
emit_report(state, assemble_docx)stores the file path instate.output_file. The email step attaches it and the Streamlit app offers it for download.
# Example of the flow (from ReceptionFlow.generate_poem)
self._run_standard(CREW_REGISTRY[CrewKey.POEM], PoemCrew(), inputs)
# _run_standard does, in order:
# kickoff_flow(crew, inputs) # run the crew
# load_or_parse_model(json_path, ...) # JSON written by output_pydantic, or the raw output
# emit_report(state, assembler closure) # build the DOCX; an error here stops the run
# state.report, state.raw_output = model, output # set only after the DOCX is built
_run_standard takes its model class, paths, title and assembler from the crew's CrewSpec in src/epic_news/crew_registry.py.
1.2. Data Contract Between Crew and Assembler¶
A common issue arises when the data structure a crew produces does not match what the assembler expects.
Best Practice:
- Ensure the data structure passed from the crew to the assembler is consistent.
- Create a clear contract (e.g., via Pydantic models) between the data source and the assembler.
- Map fields explicitly to prevent mismatches (e.g.,
link→url,published→date).
1.3. Failures Stop the Run¶
A report that cannot be built raises. There is no placeholder report, and nothing partial is emailed. If more than half the narrated sections of a report fail, the assembler aborts instead of writing a document full of placeholders.
Markdown Link Parsing¶
Issue: Incorrect regex can fail to parse Markdown links [text](url).
Solution: Use re.search() with a non-greedy pattern.
# ✅ CORRECT - Finds a link anywhere in the string
import re
match = re.search(r"\[(.*?)\]\((.*?)\)", text_with_link)
if match:
link_text = match.group(1)
link_url = match.group(2)
2. Information Retrieval Strategy¶
The project prioritizes data freshness over a static knowledge base. Instead of a traditional RAG that can become stale, agents use a suite of real-time tools to fetch information on demand.
2.1. Core Principle: Real-Time Retrieval¶
Financial markets are highly dynamic. To provide accurate and timely analysis, agents retrieve live data from the web for every task.
2.2. Key Retrieval Tools¶
HybridSearchTool: Cascading search with Perplexity (primary) → Brave → Serper fallback chain for reliable web searches.- ScraperFactory-selected scraper (
get_scraper()): Centralized website scraping; defaults toScrapeNinjaTool. Override viaWEB_SCRAPER_PROVIDER(scrapeninja,firecrawl). Direct Firecrawl usage is deprecated in crews. YahooFinanceNewsTool: For fetching the latest financial news for a specific ticker, providing timely market-moving information.
2.3. Why Not a Traditional RAG?¶
- Data Staleness: A vector database would require constant, resource-intensive updates.
- Scope Limitation: A pre-populated database is limited, whereas live tools can access the entire public web.
Agents share information within a crew through task context, not through a vector store.
3. Case Study: Refactoring the Sales Prospecting Report¶
The evolution of the SalesProspectingReport provides a clear example of applying architectural principles to solve real-world data challenges.
3.1. The Initial Problem¶
The first version of the SalesProspectingReport relied on a highly complex, nested Pydantic model called StructuredDataReport. This model was designed to capture a wide array of metrics, KPIs, data series, and tables.
Challenge: The data generated by the LLM agents did not align with this rigid and complex structure. The output was a simple, flat JSON with fields like company_overview and key_contacts. As a result, the Pydantic validation failed, and the rendering process broke.
3.2. The Solution: Data-Centric Refactoring¶
Instead of forcing the agents to conform to an overly complex model, the architecture was adapted to the data.
-
Model Simplification: The
SalesProspectingReportPydantic model was completely redesigned to match the actual data being produced. The dependency onStructuredDataReportwas removed, and a new, simpler structure was implemented:company_overview: strkey_contacts: List[KeyContact](with a newKeyContactsub-model)approach_strategy: strremaining_information: str
-
Assembler Update: The sales prospecting assembler was rewritten to work with the new, simpler model. This involved:
- Removing the logic for rendering metrics and KPIs.
- Adding sections for
company_overview,key_contacts(a table), andremaining_information.
-
Data File Correction: The
debug/repair_attempt...jsonfile was updated to conform to the new, simpler Pydantic model, ensuring that tests and local development would work correctly.
3.3. Architectural Lessons¶
- Model the Data You Have: Design Pydantic models that reflect the actual data being generated, not an idealized version.
- Simplicity Over Complexity: A simpler, flatter data structure is often more robust and easier to work with than a deeply nested one.
- Decouple Rendering from Data Structure: While the assembler needs to understand the data, the refactoring was made easier because the report logic was contained in one assembler and not scattered across the application.