Spaces:
Paused
Paused
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}")
|