File size: 6,529 Bytes
4d31de0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
"""
Financial Analyst Query Engine.

Wraps the RecursiveRetriever with a GPT-4o powered query engine
that applies financial formatting rules, comparison logic, citations,
and guardrails for out-of-scope questions.

Supports any company's financial documents dynamically.
"""

import logging

from llama_index.core.prompts import PromptTemplate
from llama_index.core.query_engine import RetrieverQueryEngine
from llama_index.core.response_synthesizers import get_response_synthesizer
from llama_index.llms.openai import OpenAI

from config import OPENAI_API_BASE, REASONING_LLM

logger = logging.getLogger(__name__)


# ─── Financial Analyst System Prompt ──────────────────────────────────────────

FINANCIAL_ANALYST_PROMPT_TEMPLATE = """\
You are a Senior Financial Analyst specializing in SEC filings and financial documents.
You have been given context from: {document_title}.
Document period/date: {document_date}.

STRICT RULES β€” follow every one:

1. NUMERICAL FORMATTING
   - Check each chunk's metadata for a "multiplier" field.
   - If multiplier is 1,000,000 and a table value is 124,300 β†’ report as "$124.30 Billion"
   - If multiplier is 1,000,000 and a table value is 4,213 β†’ report as "$4.21 Billion"
   - If multiplier is 1,000,000 and a table value is 750 β†’ report as "$750 Million"
   - Always use $ prefix for monetary values. Use "Billion" for values β‰₯ 1,000 (in millions), "Million" otherwise.

2. COMPARISON ENGINE
   - For any "growth", "change", "increase", "decrease", or "YoY" query:
     Formula: ((Current Period - Prior Period) / Prior Period) Γ— 100
   - You MUST have BOTH the current and prior period data.
   - If you cannot find both periods in the retrieved context, state this explicitly:
     "I can only find data for [period]. The comparison period is not available in the retrieved context."

3. CITATIONS
   - Every factual claim MUST include a page citation in parentheses: "(Page X)"
   - When referencing multiple sources: "(Pages 4, 17)"
   - Do NOT make claims without citations.

4. STRUCTURED OUTPUT
   - If your answer involves more than 2 data points, present them in a Markdown table.
   - Include columns for: Metric, Value, Period, and Page Reference.

5. GUARDRAILS
   - If the question asks about data NOT in the provided filing (e.g., future forecasts,
     other companies not in this document, other time periods not in the filing), respond EXACTLY with:
     "This data is not available in the provided filing."
   - Do NOT hallucinate, extrapolate, or guess. If uncertain, say so.

6. CONTEXT VERIFICATION
   - Before answering, verify you have the necessary data in the provided context.
   - If the context is insufficient, explain what's missing rather than guessing.

---------------------
CONTEXT FROM FILING:
{{context_str}}
---------------------

USER QUESTION: {{query_str}}

Provide your analysis following ALL rules above:"""


def get_financial_prompt(document_title: str, document_date: str) -> PromptTemplate:
    """Create a financial analyst prompt with document-specific details."""
    prompt_text = FINANCIAL_ANALYST_PROMPT_TEMPLATE.format(
        document_title=document_title,
        document_date=document_date,
    )
    return PromptTemplate(prompt_text)


# ─── Query Engine Builder ─────────────────────────────────────────────────────

def build_query_engine(
    recursive_retriever,
    document_title: str = "Financial Document",
    document_date: str = "",
) -> RetrieverQueryEngine:
    """
    Build a RetrieverQueryEngine with the financial analyst prompt
    and GPT-4o as the synthesis LLM.
    """
    llm = OpenAI(
        model=REASONING_LLM,
        api_base=OPENAI_API_BASE,
        temperature=0.0,
        max_tokens=512
    )

    prompt = get_financial_prompt(document_title, document_date)

    response_synthesizer = get_response_synthesizer(
        llm=llm,
        text_qa_template=prompt,
        response_mode="compact",
    )

    query_engine = RetrieverQueryEngine(
        retriever=recursive_retriever,
        response_synthesizer=response_synthesizer,
    )

    logger.info(f"Query engine ready (LLM: {REASONING_LLM}, Document: {document_title})")
    return query_engine


# ─── Response Formatting ─────────────────────────────────────────────────────

def format_response(response) -> str:
    """
    Post-process the LLM response to verify formatting requirements.
    """
    text = str(response)

    # Log source nodes for debugging
    if hasattr(response, 'source_nodes') and response.source_nodes:
        logger.info(f"Response sourced from {len(response.source_nodes)} nodes:")
        for i, node in enumerate(response.source_nodes):
            meta = node.metadata if hasattr(node, 'metadata') else {}
            page = meta.get('page_label', '?')
            section = meta.get('section_title', '?')
            is_table = meta.get('is_table', False)
            node_type = "TABLE" if is_table else "TEXT"
            logger.info(f"  [{i+1}] {node_type} | Page {page} | Section: {section}")

    return text


# ─── Interactive Query Loop ───────────────────────────────────────────────────

def interactive_query(query_engine):
    """
    Run an interactive REPL for querying the financial document.
    """
    print("\n" + "=" * 70)
    print("  FINANCIAL DOCUMENT ANALYST")
    print("  Type your question, or 'quit' to exit.")
    print("=" * 70 + "\n")

    while True:
        try:
            question = input("\nπŸ“Š Your question: ").strip()
        except (EOFError, KeyboardInterrupt):
            print("\nGoodbye!")
            break

        if not question:
            continue
        if question.lower() in ("quit", "exit", "q"):
            print("Goodbye!")
            break

        print("\n⏳ Analyzing...\n")

        try:
            response = query_engine.query(question)
            formatted = format_response(response)
            print("─" * 70)
            print(formatted)
            print("─" * 70)
        except Exception as e:
            logger.error(f"Query failed: {e}")
            print(f"❌ Error: {e}")