Financial Report Analyzer with Haystack

Build a RAG pipeline that turns 100-page SEC filings into analyst briefs.

Introduction

30 Second Summary

Every publicly traded company files a 10-K report with the SEC each year. These filings run over 100 pages of dense financial data, legal language, and accounting details that take analysts hours to parse.

In this project, you will build a command-line Financial Report Analyzer that ingests any company's 10-K filing and produces structured one-page analyst briefs using Haystack's RAG pipeline framework and Google's Gemma 4 12B model.

What You'll Build

You feed it any company's SEC 10-K PDF, pick an analysis mode, and within seconds get a structured brief highlighting the exact financial metrics and risk factors that matter most.

By the end of this project, you'll have:

  • A reusable tool that accepts any company's 10-K PDF and produces a structured analysis without reading 100+ pages manually.
  • Two professional analysis modes: Risk Detection (severity-rated material risks, audit flags, and non-GAAP concerns) and Financial Health Overview (revenue, margins, cash flow, and KPIs with actual numbers and trend directions).
  • A complete RAG pipeline that intelligently retrieves only the relevant sections from the filing using vector embeddings rather than processing the entire document blindly.
  • Secret Mission: Add a Compare Mode that runs both analyses and produces a combined executive brief with a Bull/Bear/Neutral verdict.

Are there any prerequisites?

You need Python 3.10 or higher installed, an IDE you are comfortable with, and a free Google AI Studio API key (which you will set up in Step 1).

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters. Think about which company's financial report you want to dig into and what you're hoping to learn.

Set Up Your Environment

Before you can build a RAG pipeline, you need the right framework in place. Haystack is a Python framework for building LLM-powered applications with typed, connectable components.

In this step, you will install Haystack and its Google AI integration, connect to Google AI Studio with an API key, and verify the entire stack works by sending a test prompt to Gemma 4 12B.

In this step, get ready to:
  • Install the Haystack framework and its dependencies.
  • Connect to Google AI Studio with your API key.
  • Verify the full stack works with a test prompt to Gemma 4 12B.
Install the required packages

Your project needs three packages: the Haystack pipeline framework, the Google Gen AI integration that connects Haystack to Google's models, and a PDF reader.

  • Install all three packages by running this command:
pip install haystack-ai==2.31.0 google-genai-haystack==4.5.0 pypdf

What does this install?

  • haystack-ai is the core pipeline framework. It gives you typed components that snap together like building blocks.
  • google-genai-haystack connects Haystack to Google's Gemma and Gemini models through the Gen AI SDK.
  • pypdf enables Haystack's PDF converter to read financial filings page by page.
  • Confirm the packages installed correctly by running:
python -c "from haystack_integrations.components.generators.google_genai import GoogleGenAIChatGenerator; print('Setup complete')"

You should see Setup complete printed with no errors.

Seeing an import error?

  • Make sure you ran the pip install command in the same terminal and Python environment where you are running the check.
  • If you use virtual environments, confirm you activated the correct one before installing.
  • help me fix this import error for google-genai-haystack
Get your Google AI Studio API key

Haystack's Google integration authenticates using an environment variable called GOOGLE_API_KEY. You will grab a key from Google AI Studio and set it in your terminal session.

  • Set the environment variable in your terminal by running the command for your operating system:

macOS

export GOOGLE_API_KEY=your-api-key-here

Windows

set GOOGLE_API_KEY=your-api-key-here

Linux

export GOOGLE_API_KEY=your-api-key-here
  • Replace your-api-key-here with the actual key you copied from Google AI Studio.

Keep your key private

Never paste your API key directly into code files or commit it to version control. Setting it as an environment variable keeps it out of your source code.

Not sure if the variable is set?

  • On macOS/Linux, run echo $GOOGLE_API_KEY and confirm your key prints out.
  • On Windows, run echo %GOOGLE_API_KEY% and confirm your key prints out.
  • If it prints nothing, re-run the export/set command and make sure there are no spaces around the = sign.
  • help me verify my GOOGLE_API_KEY environment variable is set correctly
Verify the full stack with a test prompt

The packages are installed and your API key is set. Now you will send an actual message to Gemma 4 12B to prove the full chain works: Haystack initializes the generator, authenticates with Google, and returns a real response.

  • Create a file called test_setup.py in your project folder and paste the following code:
from haystack_integrations.components.generators.google_genai import GoogleGenAIChatGenerator
from haystack.dataclasses import ChatMessage

# Initialize the generator with Gemma 4 12B
generator = GoogleGenAIChatGenerator(model="gemma-4-12b-it")

# Send a test message to confirm the connection works
response = generator.run(messages=[ChatMessage.from_user("What is a 10-K filing?")])

# Print the model's response
print(response["replies"][0].text)

What does this script do?

  • GoogleGenAIChatGenerator(model="gemma-4-12b-it") creates a Haystack component that talks to Google's Gemma 4 12B model. It reads your GOOGLE_API_KEY automatically from the environment.
  • ChatMessage.from_user(...) wraps your text into the message format that the generator expects.
  • response["replies"][0].text extracts the model's text response from the structured output.

Before you run this, what do you expect to see? A financial explanation from the model, or an error?

  • Run the test script:
python test_setup.py

You should see a paragraph explaining what a 10-K filing is. The exact wording varies, but any coherent text response confirms your API key, model access, and Haystack wiring all work.

Seeing a model not found error?

If gemma-4-12b-it is not available in your region or account, change the model name to gemma-4-26b-a4b-it in your script. This is a confirmed alternative on Google AI Studio's free tier.

  • If you get an authentication error, double-check that your GOOGLE_API_KEY environment variable is set in the same terminal session where you ran the script.
  • If you see a timeout, try running the script again. First requests to the model can take a few seconds to warm up.
  • help me fix my Gemma 4 connection through Haystack

Your environment is ready. Next up, you will load a real 10-K filing and see why a naive approach falls flat before building a proper RAG pipeline.

Load a 10-K and Ask a Naive Question

Your Haystack setup is verified and Gemma 4 12B is responding to prompts. Now it's time to throw a real financial document at it and see what happens.

SEC 10-K filings are the comprehensive annual reports that public companies file with the Securities and Exchange Commission. They run 100+ pages covering revenue, risks, strategy, and financial statements. The simplest approach is to dump that text straight into an LLM and ask for insights. Let's try it and see what comes back.

In this step, get ready to:
  • Download a 10-K PDF filing from SEC EDGAR.
  • Send a truncated portion of the filing to Gemma 4 12B.
  • Observe why the naive approach falls short for financial analysis.
Download a 10-K filing from SEC EDGAR

A 10-K is the annual report every public company files with the SEC. It contains audited financial statements, risk factors, and management's discussion of results. You need a real one to test against.

  • Open https://www.sec.gov/cgi-bin/browse-edgar in your browser.
  • Type a company name or ticker (e.g. AAPL for Apple) in the Company name field.
  • Set the Filing type filter to 10-K and click Search.
  • Click Documents next to the most recent 10-K filing.
  • Find the PDF link on the filing page and download it.
  • Save the PDF into your project folder (the same folder where you will create financial_analyzer.py).

Any 10-K works

Apple, Tesla, Microsoft, or any public company will work. Pick one you find interesting. The PDF will typically be 80-200 pages long.

Send truncated text to Gemma

The most obvious approach to analyzing a financial report is to read the PDF, grab some text, and ask the LLM a question about it. Let's try exactly that.

  • Create a file called financial_analyzer.py in your project folder and paste the following code:
import os
from pathlib import Path

from haystack.components.converters import PyPDFToDocument
from haystack_integrations.components.generators.google_genai import GoogleGenAIChatGenerator
from haystack.dataclasses import ChatMessage

# Use Gemma 4 12B for generation
MODEL_NAME = "gemma-4-12b-it"

# Load the 10-K PDF and grab a small portion of text
pdf_path = "[[PDF_FILENAME=\"your-10k.pdf\"]]"
converter = PyPDFToDocument()
result = converter.run(sources=[Path(pdf_path)])
doc = result["documents"][0]
truncated_text = doc.content[:3000]

# Send the truncated text to Gemma with a broad question
generator = GoogleGenAIChatGenerator(model=MODEL_NAME)
messages = [ChatMessage.from_user(f"Based on this excerpt from a 10-K filing, what are the key financial risks and revenue drivers?\n\n{truncated_text}")]
response = generator.run(messages=messages)
print(response["replies"][0].text)

What does this code do?

  • PyPDFToDocument reads every page of the PDF and converts them into Haystack Document objects.
  • doc.content[:3000] takes only the first 3000 characters of the first page's text. That's roughly one page of content from a 100+ page filing.
  • GoogleGenAIChatGenerator sends that tiny slice to Gemma 4 12B with a question about financial risks and revenue drivers.
  • Replace the PDF filename variable with the actual name of the 10-K PDF you downloaded.
  • Save financial_analyzer.py.

Before you run this, think about what you expect. Do you think Gemma will give you specific revenue numbers, risk severity ratings, and margin percentages from just 3000 characters of a 100+ page document?

  • Run the script:
python financial_analyzer.py

You will see Gemma respond. But look closely at what comes back.

Identify the problem

The response is vague, generic, and lacks the specific dollar figures an analyst would need. It probably mentions broad risks like "market conditions" or "competition" without severity ratings or quantified impact.

Why is the response so weak?

The model only saw roughly one page of a 100+ page filing. The first 3000 characters are typically the cover page and table of contents, not the actual Risk Factors or MD&A sections where the real financial data lives.

The LLM has no way to find the relevant sections on its own. It can only work with what you give it. This is the core problem that RAG solves: targeted retrieval of the right content before generation.

Script not running?

  • If you see a file not found error, make sure the PDF filename in your code matches the exact file name in your project folder (including the .pdf extension).
  • If you see an API error, confirm your GOOGLE_API_KEY environment variable is still set in the same terminal session.

Help me debug my financial_analyzer.py script

✔️ Awesome, I've got everything!

Great. Double check you have saved financial_analyzer.py and confirmed the vague response.

ⓧ I'd like to double check the full code

import os
from pathlib import Path

from haystack.components.converters import PyPDFToDocument
from haystack_integrations.components.generators.google_genai import GoogleGenAIChatGenerator
from haystack.dataclasses import ChatMessage

MODEL_NAME = "gemma-4-12b-it"

pdf_path = "your-10k.pdf"
converter = PyPDFToDocument()
result = converter.run(sources=[Path(pdf_path)])
doc = result["documents"][0]
truncated_text = doc.content[:3000]

generator = GoogleGenAIChatGenerator(model=MODEL_NAME)
messages = [ChatMessage.from_user(f"Based on this excerpt from a 10-K filing, what are the key financial risks and revenue drivers?\n\n{truncated_text}")]
response = generator.run(messages=messages)
print(response["replies"][0].text)

You have proven that the naive approach fails. Dumping a random slice of a 100+ page document into an LLM produces surface-level commentary, not actionable financial analysis. In the next step, you will build a proper RAG indexing pipeline that chunks the entire document and uses vector embeddings to find exactly the sections that matter for any financial question.

Build the RAG Indexing Pipeline

In the previous step you saw the problem firsthand. The LLM received a random slice of a 100+ page filing and produced a vague, generic response with no specific figures.

The fix is targeted retrieval. By chunking the entire document into passages and creating vector embeddings for each chunk, you enable semantic search that finds exactly the sections relevant to any financial question.

In this step, get ready to:
  • Build a five-component indexing pipeline that converts, cleans, chunks, embeds, and stores your PDF.
  • Run the pipeline on your 10-K filing and confirm the chunk count.
  • Test semantic retrieval to prove relevant financial data comes back.
Replace your script with new imports

The naive script served its purpose. Now you need a clean slate with all the Haystack components required for a proper RAG pipeline.

  • Open financial_analyzer.py in your IDE.
  • Select all the existing code and delete it.
  • Replace the file contents with these imports and configuration:
import os
from pathlib import Path

from haystack import Pipeline
from haystack.components.converters import PyPDFToDocument
from haystack.components.preprocessors import DocumentCleaner, DocumentSplitter
from haystack.components.writers import DocumentWriter
from haystack.document_stores.in_memory import InMemoryDocumentStore
from haystack.components.retrievers.in_memory import InMemoryEmbeddingRetriever
from haystack_integrations.components.embedders.google_genai import (
    GoogleGenAIDocumentEmbedder,
    GoogleGenAITextEmbedder,
)

EMBEDDING_MODEL = "gemini-embedding-001"

What does this code do?

You are importing five pipeline components that each handle one stage of document processing: converting PDFs, cleaning text, splitting into chunks, creating embeddings, and writing to a store.

The EMBEDDING_MODEL constant points to Google's gemini-embedding-001 model, which converts text into numerical vectors for semantic search.

Define the indexing pipeline function

Each component in the pipeline handles one responsibility. You chain them together so data flows automatically from PDF conversion through to the document store.

  • Below the EMBEDDING_MODEL line, add the following function:
def build_indexing_pipeline(document_store):
    pipeline = Pipeline()
    pipeline.add_component("converter", PyPDFToDocument())
    pipeline.add_component("cleaner", DocumentCleaner())
    pipeline.add_component(
        "splitter",
        DocumentSplitter(split_by="word", split_length=500, split_overlap=50),
    )
    pipeline.add_component(
        "embedder",
        GoogleGenAIDocumentEmbedder(model=EMBEDDING_MODEL),
    )
    pipeline.add_component("writer", DocumentWriter(document_store=document_store))

    pipeline.connect("converter", "cleaner")
    pipeline.connect("cleaner", "splitter")
    pipeline.connect("splitter", "embedder")
    pipeline.connect("embedder", "writer")

    return pipeline

What does this code do?

  • PyPDFToDocument converts each PDF page into a Haystack Document object.
  • DocumentCleaner removes noise like extra whitespace and headers.
  • DocumentSplitter breaks the text into ~500-word chunks with 50-word overlap so no sentence gets cut off at a boundary.
  • GoogleGenAIDocumentEmbedder turns each chunk into a vector embedding for semantic search.
  • DocumentWriter stores all embedded chunks in the in-memory document store.

The pipeline.connect() calls wire data flow between components so each stage feeds the next automatically.

Why 500-word chunks with 50-word overlap?

Financial documents are dense with numbers and tables. 500-word chunks preserve enough context around financial figures while staying within embedding model token limits. The 50-word overlap ensures context is not lost at chunk boundaries.

Run the indexing pipeline on your 10-K
  • Below the function you just wrote, add the following code to create a document store and run the pipeline:
pdf_path = "your-10k.pdf"
document_store = InMemoryDocumentStore()
indexing_pipeline = build_indexing_pipeline(document_store)
indexing_pipeline.run({"converter": {"sources": [Path(pdf_path)]}})
print(f"Indexed {document_store.count_documents()} document chunks into the store.")

What does this code do?

InMemoryDocumentStore creates a lightweight vector store that lives in RAM. No external database needed.

The pipeline.run() call passes your PDF path into the converter component, which kicks off the entire chain. Every chunk flows through cleaning, splitting, embedding, and storage automatically.

  • Replace "your-10k.pdf" with the actual filename of the 10-K PDF you downloaded in the previous step.
  • Save financial_analyzer.py.
  • Run the script by running:
python financial_analyzer.py

You should see output like:

Indexed 423 document chunks into the store.

Your exact number depends on the length of your PDF. A typical 10-K produces 300-600 chunks.

Seeing a file not found error?

  • Make sure your PDF file is in the same folder as your script. If it is in a subfolder, include the relative path.
  • Check the filename matches exactly, including the extension and any spaces.

Help me debug my indexing pipeline file path error.

Test semantic retrieval on the embedded chunks

The document store is full of embedded chunks. Now you need to prove that semantic search actually finds relevant financial data instead of random text.

  • Add the following code at the bottom of financial_analyzer.py, below the print statement:
text_embedder = GoogleGenAITextEmbedder(model=EMBEDDING_MODEL)
embedding_result = text_embedder.run(text="revenue growth and key financial metrics")
retriever = InMemoryEmbeddingRetriever(document_store=document_store, top_k=5)
retrieval_result = retriever.run(query_embedding=embedding_result["embedding"])
print(retrieval_result["documents"][0].content)

What does this code do?

  • GoogleGenAITextEmbedder converts your search query into the same vector space as the document chunks.
  • InMemoryEmbeddingRetriever with top_k=5 finds the 5 chunks most semantically similar to your query.
  • The print statement shows the top matching chunk so you can verify it contains actual financial data.
  • Save financial_analyzer.py.

Before you run this, what do you expect the retrieved chunk to contain? Think about what "revenue growth and key financial metrics" should match in a 10-K filing.

  • Run the script again:
python financial_analyzer.py

After the chunk count, you should see a block of text containing actual financial data from the report. Look for specific dollar figures, revenue numbers, or growth percentages. This is the semantic retrieval working. It found the relevant financial section without you telling it which page to look at.

Retrieved chunk looks like boilerplate?

  • If the first chunk is a table of contents or legal boilerplate, your PDF might have a lot of front matter. The retriever still works. Try changing the query string to something more specific to your company's business.
  • Confirm your GOOGLE_API_KEY environment variable is still set. The embedder needs it to create vectors.

Help me debug my Haystack retrieval results.

✔️ Awesome, I've got everything!

Great. Make sure you have saved financial_analyzer.py.

ⓧ I'd like to double check the full code

Your financial_analyzer.py should contain these sections in order. Scroll up and confirm each one is present:

  • Imports and EMBEDDING_MODEL constant (the first chunk you added).
  • The build_indexing_pipeline() function with five components and four connections.
  • Runtime code that creates the InMemoryDocumentStore, runs the pipeline, and prints the chunk count.
  • Retrieval test using GoogleGenAITextEmbedder and InMemoryEmbeddingRetriever with top_k=5.

Your document store is loaded and semantic retrieval is proven. Next up, you will build the analysis pipeline that feeds those retrieved chunks into specialized prompts and produces structured financial reports.

Build the Analysis Pipeline with Two Modes

Your indexing pipeline proved that the document store contains relevant financial data from the 10-K. But raw text chunks are not an analyst brief.

In this step, you will build specialized prompts that instruct Gemma to extract exactly the metrics that SEC standards prioritize. You will organize them into two analysis modes: Risk Detection and Financial Health Overview.

In this step, get ready to:
  • Create two structured prompt templates aligned with SEC 10-K analysis standards.
  • Build a reusable query pipeline function that connects embedder, retriever, prompt builder, and generator.
  • Run the pipeline in Financial Health Overview mode and see structured metrics extracted from your filing.
Create the two prompt templates

Each analysis mode needs a specialized prompt that tells Gemma exactly what to extract and how to structure it. The prompts use Jinja2 template variables so ChatPromptBuilder can automatically inject retrieved documents at runtime.

  • In financial_analyzer.py, find the existing imports at the top of the file.
  • Replace the entire import block and constant section with the following updated version that adds three new imports and a model name constant:
import os
from pathlib import Path

from haystack import Pipeline
from haystack.components.converters import PyPDFToDocument
from haystack.components.preprocessors import DocumentCleaner, DocumentSplitter
from haystack.components.writers import DocumentWriter
from haystack.document_stores.in_memory import InMemoryDocumentStore
from haystack.components.retrievers.in_memory import InMemoryEmbeddingRetriever
# New imports for the query pipeline
from haystack.components.builders import ChatPromptBuilder
from haystack.dataclasses import ChatMessage
from haystack_integrations.components.generators.google_genai import (
    GoogleGenAIChatGenerator,
)
from haystack_integrations.components.embedders.google_genai import (
    GoogleGenAIDocumentEmbedder,
    GoogleGenAITextEmbedder,
)


# Model constants
MODEL_NAME = "gemma-4-12b-it"
EMBEDDING_MODEL = "gemini-embedding-001"

What changed in the imports?

  • ChatPromptBuilder renders Jinja2 templates and automatically exposes template variables (like documents and query) as pipeline input ports.
  • ChatMessage is how Haystack represents messages passed between components in a chat pipeline.
  • GoogleGenAIChatGenerator connects to Google AI Studio and sends prompts to Gemma 4 12B for generation.
  • MODEL_NAME stores the model endpoint so you can change it in one place if needed.
  • Next, scroll to the bottom of the file and delete all the test code below the build_indexing_pipeline function (the lines starting with pdf_path = "your-10k.pdf" through to the end).
  • Save financial_analyzer.py and run it to confirm the new imports load without errors:
python financial_analyzer.py

You should see no output and no errors. The file now contains only clean function definitions and constants.

Seeing a ModuleNotFoundError?

  • Verify you installed google-genai-haystack in Step 1. The GoogleGenAIChatGenerator class lives in that package.
  • Check for typos in the import paths. The generators module is haystack_integrations.components.generators.google_genai (not google_ai).

Still stuck? Help me fix my import errors in financial_analyzer.py

  • Now add the Risk Detection prompt template below the EMBEDDING_MODEL constant. Copy this code into financial_analyzer.py:
RISK_DETECTION_TEMPLATE = [ChatMessage.from_user(
    """You are a senior financial risk analyst reviewing a company's SEC 10-K filing.
Based on the following excerpts from the filing, produce a structured Risk Detection Report.

Extract and organize the following:

1. MATERIAL RISK FACTORS: List the top 5 most significant risks with specific details and quantification where available. Rate each as HIGH, MEDIUM, or LOW severity.
2. AUDIT CONCERNS: Note the auditor's opinion type, any internal control weaknesses, or changes in accountants.
3. NON-GAAP RED FLAGS: Identify any non-GAAP measures that lack proper reconciliation or appear to exclude recurring expenses.
4. CRITICAL ACCOUNTING ESTIMATES: Flag estimates with high uncertainty that could materially affect reported results.
5. KNOWN TRENDS AND UNCERTAINTIES: List specific trends or uncertainties management discloses that could impact future performance.

If information is not available in the provided context, state "Not found in provided sections."

Context from the 10-K filing:
{% for document in documents %}
---
{{ document.content }}
---
{% endfor %}

Company being analyzed: {{ query }}

Produce a concise, structured one-page Risk Detection Report:"""
)]

What does this template do?

The template instructs Gemma to act as a financial risk analyst and extract five categories of risk information from the retrieved sections. Each category aligns with what SEC reviewers focus on when examining 10-K filings.

The {% for document in documents %} loop is a Jinja2 block that ChatPromptBuilder fills automatically with the chunks returned by the retriever. The {{ query }} variable receives the company name at runtime.

  • Add the Financial Health Overview template directly below RISK_DETECTION_TEMPLATE:
FINANCIAL_HEALTH_TEMPLATE = [ChatMessage.from_user(
    """You are a senior equity research analyst reviewing a company's SEC 10-K filing.
Based on the following excerpts from the filing, produce a structured Financial Health Overview.

Extract and organize the following:

1. REVENUE AND GROWTH: Total revenue, year-over-year growth rate, and key growth drivers (quantified where possible).
2. PROFITABILITY: Gross margin, operating margin, net income, and earnings per share. Note trends (improving/declining/stable).
3. CASH FLOW HEALTH: Operating cash flow, free cash flow (operating minus capex), and major investing/financing activities.
4. LIQUIDITY AND SOLVENCY: Cash and equivalents, working capital, current ratio, total debt, and debt-to-equity ratio.
5. SEGMENT PERFORMANCE: Revenue and operating income by business segment, noting which segments drive growth.
6. KEY PERFORMANCE INDICATORS: Any KPIs management highlights, with their definitions and period-over-period changes.

For each metric, provide the actual number and the trend direction.
If information is not available in the provided context, state "Not found in provided sections."

Context from the 10-K filing:
{% for document in documents %}
---
{{ document.content }}
---
{% endfor %}

Company being analyzed: {{ query }}

Produce a concise, structured one-page Financial Health Overview:"""
)]

How do the two modes differ?

Risk Detection focuses on what could go wrong: material threats, audit weaknesses, and accounting uncertainties. Financial Health Overview focuses on what IS happening: actual revenue numbers, margins, and performance trends.

Together they give you both sides of the analyst picture from the same filing.

  • Save financial_analyzer.py and run it again to confirm the template syntax is valid:
python financial_analyzer.py

No output and no errors means both templates parsed correctly.

Seeing a SyntaxError near the triple quotes?

  • Make sure the opening """ is on the same line as ChatMessage.from_user( and the closing """ is followed by \n)] on the next line.
  • Check that no stray characters appear between the two template definitions.

Need help? Help me fix my template syntax error

Build the query pipeline function

The query pipeline chains four components: embed the search query, retrieve matching chunks, render the prompt template with those chunks, and generate the structured analysis.

  • Add the retrieval queries dictionary and the build_query_pipeline function below FINANCIAL_HEALTH_TEMPLATE but above the existing build_indexing_pipeline function:
RETRIEVAL_QUERIES = {
    "risk": "material risks audit opinion internal controls non-GAAP measures critical accounting estimates known trends uncertainties",
    "health": "total revenue net income earnings per share gross margin operating margin cash flow liquidity debt segments key performance indicators",
}


def build_query_pipeline(document_store, mode):
    # Select the prompt template based on the chosen analysis mode
    template = RISK_DETECTION_TEMPLATE if mode == "risk" else FINANCIAL_HEALTH_TEMPLATE

    pipeline = Pipeline()
    pipeline.add_component(
        "text_embedder", GoogleGenAITextEmbedder(model=EMBEDDING_MODEL)
    )
    pipeline.add_component(
        "retriever",
        InMemoryEmbeddingRetriever(document_store=document_store, top_k=15),
    )
    pipeline.add_component("prompt_builder", ChatPromptBuilder(template=template))
    pipeline.add_component("generator", GoogleGenAIChatGenerator(model=MODEL_NAME))

    # Wire the pipeline: embed query -> retrieve chunks -> build prompt -> generate
    pipeline.connect("text_embedder.embedding", "retriever.query_embedding")
    pipeline.connect("retriever.documents", "prompt_builder.documents")
    pipeline.connect("prompt_builder.prompt", "generator.messages")

    return pipeline

What does this pipeline do?

  • RETRIEVAL_QUERIES maps each mode to a keyword-rich search string that targets the right sections of the filing.
  • text_embedder converts the retrieval query into a vector embedding using gemini-embedding-001.
  • retriever finds the top 15 most semantically similar chunks from the document store.
  • prompt_builder injects those 15 chunks into the Jinja2 template, filling the documents loop automatically.
  • generator sends the completed prompt to Gemma 4 12B and returns the structured analysis.
  • Save financial_analyzer.py.

Not sure where to place the code?

The RETRIEVAL_QUERIES dict goes right after FINANCIAL_HEALTH_TEMPLATE (after the closing )]). Then build_query_pipeline goes directly below that, before build_indexing_pipeline.

Help me figure out where to place build_query_pipeline

Run the pipeline in Financial Health mode

The function is defined but untested. To verify everything works end-to-end, you will add temporary test code that indexes your PDF and runs the health analysis.

  • Add this test code at the very bottom of financial_analyzer.py, below the build_indexing_pipeline function:
# Temporary test code - will be replaced in the next step
pdf_path = "your-10k.pdf"
document_store = InMemoryDocumentStore()
indexing_pipeline = build_indexing_pipeline(document_store)
indexing_pipeline.run({"converter": {"sources": [Path(pdf_path)]}})
print(f"Indexed {document_store.count_documents()} chunks.")

print("Running Financial Health analysis...")
query_pipeline = build_query_pipeline(document_store, "health")
result = query_pipeline.run({
    "text_embedder": {"text": RETRIEVAL_QUERIES["health"]},
    "prompt_builder": {"query": "Apple Inc."},
})
print(result["generator"]["replies"][0].text)

What does this test code do?

  • It re-indexes your 10-K PDF into a fresh in-memory store (same as before).
  • It builds the query pipeline in "health" mode, which selects the Financial Health template.
  • The retrieval query targets revenue, margins, cash flow, and KPI terms so the retriever pulls the right sections.
  • The company name ("Apple Inc.") fills the {{ query }} variable in the template. Replace it with whatever company your PDF is for.

Before you run this, what do you expect the output to look like? Do you think it will produce vague summaries like the naive approach in Step 2, or specific numbers?

  • Save financial_analyzer.py and run it:
python financial_analyzer.py

You should see a structured Financial Health Overview with actual dollar figures, margin percentages, and trend directions pulled from your 10-K filing. This is a dramatic improvement over the vague, truncated response from Step 2.

Why is this output so much better?

The RAG pipeline retrieves the 15 most relevant chunks using semantic search, not a random truncation. Gemma sees the actual revenue tables, margin disclosures, and cash flow statements instead of a random slice of the filing.

The structured prompt then tells Gemma exactly what to extract and how to format it, producing a one-page analyst brief rather than a freeform summary.

  • Once you have confirmed the output, delete the temporary test code you added at the bottom (everything below build_indexing_pipeline). The next step will add a proper interactive interface.
  • Save financial_analyzer.py.

Seeing a 404 or model not found error?

  • If gemma-4-12b-it is unavailable, change MODEL_NAME to "gemma-4-26b-a4b-it" as a fallback.
  • Confirm your GOOGLE_API_KEY environment variable is still set in your terminal session.
  • If the response is cut off or empty, the PDF may have too few parseable pages. Try a different 10-K filing.

Need help? Help me debug my query pipeline run

✔️ Awesome, I've got everything!

Great. Double check you have saved financial_analyzer.py with the test code removed.

ⓧ I'd like to double check the full code

Your financial_analyzer.py should contain these sections in order. Scroll through your file and confirm each one is present.

  • Imports (including ChatPromptBuilder, ChatMessage, GoogleGenAIChatGenerator).
  • MODEL_NAME and EMBEDDING_MODEL constants.
  • RISK_DETECTION_TEMPLATE (ChatMessage list with 5 extraction categories).
  • FINANCIAL_HEALTH_TEMPLATE (ChatMessage list with 6 extraction categories).
  • RETRIEVAL_QUERIES dict with "risk" and "health" keys.
  • build_indexing_pipeline() function (unchanged from Step 3).
  • build_query_pipeline() function connecting text_embedder, retriever, prompt_builder, and generator.
  • No test code at the bottom of the file.

Your analysis pipeline is working. Next up, you will wrap both modes in an interactive tool that accepts any PDF and lets you choose your analysis on the fly.

Wire the Interactive Tool

Your RAG pipeline works, but right now switching between analysis modes or testing a different PDF means editing code every time.

In this step, you will wrap everything into an interactive command-line tool. The user provides a PDF path, a company name, and picks an analysis mode. The tool handles the rest and prints a structured report with progress updates at every stage.

In this step, get ready to:
  • Create an analyze_report function that orchestrates ingestion, retrieval, and generation with progress messages.
  • Build an interactive main function with PDF validation, company name input, and mode selection.
  • Test end-to-end with both analysis modes and a second company's 10-K.
Create the analyze_report orchestration function

The analyze_report function ties together everything you built in the previous steps. It takes a PDF path, a mode, and a company name, then orchestrates the full pipeline with numbered progress messages so the user knows what is happening.

  • In financial_analyzer.py, scroll down below the build_query_pipeline function and add the first half of analyze_report by copying this code:
def analyze_report(pdf_path, mode, company_name):
    print(f"\n{'=' * 60}")
    print("  10-K Financial Report Analyzer")
    print(f"{'=' * 60}")
    print(f"  File: {pdf_path}")
    print(f"  Mode: {'Risk Detection' if mode == 'risk' else 'Financial Health Overview'}")
    print(f"  Company: {company_name}")
    print(f"{'=' * 60}\n")

    print("[1/4] Ingesting PDF...")
    document_store = InMemoryDocumentStore()
    indexing_pipeline = build_indexing_pipeline(document_store)
    indexing_pipeline.run({"converter": {"sources": [Path(pdf_path)]}})
    doc_count = document_store.count_documents()
    print(f"       Indexed {doc_count} document chunks into the store.")

What does this code do?

The function prints a formatted header showing the file, mode, and company being analyzed. Then it runs the indexing pipeline you built in the previous step, converting the PDF into embedded chunks.

The [1/4] progress label tells the user exactly where they are in the process. The chunk count confirms ingestion succeeded.

  • Immediately below the code you just added (still inside the same function), add the query execution and output portion:
    print(f"[2/4] Building {mode} analysis pipeline...")
    query_pipeline = build_query_pipeline(document_store, mode)

    retrieval_query = RETRIEVAL_QUERIES[mode]
    print("[3/4] Retrieving relevant sections...")
    print("[4/4] Generating structured analysis with Gemma 4 12B...\n")

    result = query_pipeline.run(
        {
            "text_embedder": {"text": retrieval_query},
            "prompt_builder": {"query": company_name},
        }
    )

    report = result["generator"]["replies"][0].text

    print("-" * 60)
    print(report)
    print("-" * 60)
    print("\nAnalysis complete.")

    return report

What does this code do?

This second half builds the query pipeline for the selected mode, retrieves the top 15 most relevant chunks using the mode-specific retrieval query from RETRIEVAL_QUERIES, and sends them to Gemma 4 12B for structured analysis.

The result is extracted from result["generator"]["replies"][0].text and printed between separator lines for readability. The function also returns the report text so it can be reused programmatically.

  • Save financial_analyzer.py.
  • Verify the function parses correctly by running:
python -c "from financial_analyzer import analyze_report; print('analyze_report loaded successfully')"

You should see analyze_report loaded successfully printed with no errors.

Seeing a SyntaxError?

Check that both halves of the function have consistent indentation. The second code block should be indented at the same level as the first (four spaces inside the function body).

Also confirm there are no stray characters between the two chunks. The second block continues directly inside analyze_report.

Help me debug my analyze_report function

Build the interactive main function

The main function handles user interaction. It prompts for inputs, validates the PDF path exists, and maps the mode choice before handing off to analyze_report.

  • Below the analyze_report function, add the main function and entry point by copying this code:
def main():
    print("\n" + "=" * 60)
    print("  10-K Financial Report Analyzer")
    print("  Powered by Haystack + Gemma 4 12B")
    print("=" * 60 + "\n")

    pdf_path = input("Enter the path to your 10-K PDF file: ").strip()
    if not Path(pdf_path).exists():
        print(f"Error: File not found at '{pdf_path}'")
        return

    company_name = input("Enter the company name (e.g., Apple Inc.): ").strip()

    print("\nChoose analysis mode:")
    print("  1. Risk Detection - flags, red flags, audit concerns, uncertainties")
    print("  2. Financial Health Overview - revenue, margins, cash flow, KPIs")
    choice = input("\nEnter 1 or 2: ").strip()

    if choice == "1":
        mode = "risk"
    elif choice == "2":
        mode = "health"
    else:
        print("Invalid choice. Defaulting to Financial Health Overview.")
        mode = "health"

    analyze_report(pdf_path, mode, company_name)


if __name__ == "__main__":
    main()

What does this code do?

The main function prints a welcome banner, then collects three inputs from the user: the PDF file path, company name, and analysis mode choice.

If the PDF path does not exist on disk, the function prints an error and exits early. This prevents the pipeline from crashing with a confusing file-not-found exception midway through processing.

The if __name__ == "__main__" guard ensures main() only runs when you execute the script directly, not when importing functions from it.

  • Save financial_analyzer.py.
Test end-to-end with both modes

The tool is complete. Time to run it against your 10-K PDF and confirm both analysis modes produce distinct, structured output.

  • Run the analyzer by executing:
python financial_analyzer.py

What will the script ask you?

The script will prompt you for three things in sequence:

  • The path to your 10-K PDF (type the filename if it is in the same folder, or the full path).
  • The company name (e.g., Apple Inc.).
  • A mode choice: enter 2 for Financial Health Overview first.

Before you press Enter on your mode choice, predict: will the output contain actual dollar figures and percentages from the filing, or generic statements?

  • Enter 2 when prompted to select Financial Health Overview.

You should see numbered progress messages followed by a structured Financial Health Overview with real revenue numbers, margin percentages, and trend directions extracted from the filing.

  • Run the script again and this time enter 1 to select Risk Detection mode:
python financial_analyzer.py

The Risk Detection report should look distinctly different. It lists specific risks with HIGH/MEDIUM/LOW severity ratings, audit concerns, and known uncertainties.

  • Finally, test generalization by running the script on a different company's 10-K filing.
  • If you do not have a second PDF, download one from SEC EDGAR at https://www.sec.gov/cgi-bin/browse-edgar. Search for any company name, find their 10-K filing, and save the HTML page as a PDF using your browser's print function.
  • Run python financial_analyzer.py with the new PDF and confirm the output references that company's specific financial data, not the first company's.

Seeing generic or empty output?

If the report lacks specific numbers, the PDF may not have converted cleanly. Some scanned PDFs produce image-only pages that PyPDFToDocument cannot read. Use a text-based filing (most EDGAR filings are text-based HTML saved as PDF).

If you see an error about the model not being found, try changing MODEL_NAME at the top of your file to gemma-4-26b-a4b-it as a fallback.

Help me troubleshoot my financial analyzer output

✔️ Awesome, I've got everything!

Great. Double check you have saved financial_analyzer.py and that both analysis modes ran successfully.

ⓧ I'd like to double check the full code

Your financial_analyzer.py should contain these sections in order. Scroll through your file and confirm each is present:

  • Imports (os, Path, Haystack components, Google GenAI integrations).
  • Constants: MODEL_NAME and EMBEDDING_MODEL.
  • RISK_DETECTION_TEMPLATE (ChatMessage with 5-section risk prompt).
  • FINANCIAL_HEALTH_TEMPLATE (ChatMessage with 6-section health prompt).
  • RETRIEVAL_QUERIES dictionary with "risk" and "health" keys.
  • build_indexing_pipeline(document_store) function.
  • build_query_pipeline(document_store, mode) function.
  • analyze_report(pdf_path, mode, company_name) function with progress messages and pipeline orchestration.
  • main() function with input prompts, file validation, and mode selection.
  • if __name__ == "__main__": main() entry point at the bottom.

Your Financial Report Analyzer is now a complete, reusable tool. Drop in any 10-K PDF, pick a mode, and get structured analysis in seconds. Next up, you will tackle the Secret Mission and add a Compare Mode that synthesizes both analyses into a single executive verdict.

Secret mission

Add Compare Mode

Your analyzer runs one mode at a time. What if you could run both analyses, then synthesize a combined executive brief with a Bull/Bear/Neutral verdict? Add a Compare Mode that weighs financial strength against risk exposure in a single pass.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally with no cloud resources or ongoing costs.

Resources you used:

  • Project file: financial_analyzer.py.
  • Downloaded 10-K PDF files in your project folder.
  • Installed Python packages: haystack-ai, google-genai-haystack, pypdf.
  • Google AI Studio API key (set as a session environment variable).

Keep everything running

No action needed. Choose this if you plan to analyze more 10-K filings or experiment with new prompt templates.

  • Your financial_analyzer.py script and any downloaded PDFs stay on disk, ready for future runs.
  • The GOOGLE_API_KEY environment variable only lasts for the current terminal session. You will need to set it again if you open a new terminal.
  • The installed pip packages remain available in your Python environment for other projects.

Pause - I'll come back to this later

There is nothing to pause. This project has no running processes, background services, or cloud resources.

  • The InMemoryDocumentStore only exists while the script is executing. It is discarded automatically when the script finishes.
  • Your files remain on disk whenever you want to come back.
  • Google AI Studio free tier does not charge for idle time. Your API key remains valid until you revoke it.

Delete - I don't want to use this again

Remove all project files and uninstall the packages you added.

  • Delete your project files by running this command from the folder containing financial_analyzer.py:
cd ~/Desktop
rm financial_analyzer.py
  • Delete any 10-K PDF files you downloaded to the same folder.
  • Uninstall the Python packages by running:
pip uninstall haystack-ai google-genai-haystack pypdf -y
  • Revoke your Google AI Studio API key by visiting https://aistudio.google.com/app/apikey and deleting the key you created for this project.
  • The GOOGLE_API_KEY environment variable disappears automatically when you close your terminal session.

Nice Work!

Nice Work!

Nice work! You just built a production-style Financial Report Analyzer that transforms dense SEC filings into structured, actionable analyst briefs using Haystack and Gemma 4 12B.

You've learned how to:

  • Built a complete RAG pipeline that ingests 100+ page PDF financial reports, chunks them into passages, and creates vector embeddings for semantic retrieval using Haystack and Google AI Studio.
  • Connected Gemma 4 12B as the LLM backbone for structured financial extraction, with specialized prompts that target exactly the metrics SEC standards prioritize.
  • Created two professional analysis modes: Risk Detection (material risks, audit flags, non-GAAP concerns) and Financial Health Overview (revenue, margins, cash flow, KPIs) that work on any company's 10-K filing.
  • Secret Mission: Added Compare Mode that synthesizes both analyses into an executive brief with a Bull/Bear/Neutral verdict referencing specific findings from both reports.

Ready to quiz yourself?