Spaces:
Sleeping
Sleeping
File size: 14,085 Bytes
76bd1fc | 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 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 | """
FinAgent β Infosys AR 2024-25 | Streamlit Q&A Interface
Run:
streamlit run app.py
"""
import json
import re
import time
import streamlit as st
# ββ Page config (must be first Streamlit call) ββββββββββββββββββββββββββββββββ
st.set_page_config(
page_title="FinAgent Β· Infosys AR Q&A",
page_icon="π",
layout="wide",
initial_sidebar_state="expanded",
)
# ββ Custom CSS ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
st.markdown(
"""
<style>
.main-title { font-size:2.2rem; font-weight:700; color:#1a3c5e; margin-bottom:0; }
.sub-title { font-size:1rem; color:#5a7fa6; margin-top:0; margin-bottom:1.5rem; }
.metric-card { background:#f0f6ff; border-radius:10px; padding:12px 18px;
border-left:4px solid #1a3c5e; margin-bottom:8px; }
.badge { display:inline-block; padding:2px 10px; border-radius:12px;
font-size:0.78rem; font-weight:600; margin-right:6px; }
.badge-blue { background:#dbeafe; color:#1e40af; }
.badge-green { background:#dcfce7; color:#166534; }
.badge-amber { background:#fef9c3; color:#854d0e; }
.badge-red { background:#fee2e2; color:#991b1b; }
.cite-chip { display:inline-block; background:#e0e7ff; color:#3730a3;
border-radius:6px; padding:1px 8px; font-size:0.8rem;
margin:2px; font-weight:500; }
.context-box { background:#fafafa; border:1px solid #e5e7eb; border-radius:8px;
padding:10px 14px; margin:6px 0; font-size:0.85rem; }
.answer-box { background:#f8faff; border-left:4px solid #3b82f6;
padding:16px 20px; border-radius:8px; }
</style>
""",
unsafe_allow_html=True,
)
# ββ Helper: colour-coded intent badge ββββββββββββββββββββββββββββββββββββββββ
INTENT_BADGE = {
"financial_metrics": ("π", "badge-blue", "Financial Metrics"),
"compliance_check": ("βοΈ", "badge-amber", "Compliance Check"),
"investment_alert": ("π", "badge-green", "Investment Alert"),
"analyst_review": ("π
", "badge-blue", "Analyst Review"),
"rag_query": ("π", "badge-blue", "General Query"),
}
def intent_badge(intent: str) -> str:
icon, cls, label = INTENT_BADGE.get(
intent, ("β", "badge-blue", intent or "Unknown")
)
return f'<span class="badge {cls}">{icon} {label}</span>'
def score_colour(score: float) -> str:
if score >= 0.8:
return "badge-green"
if score >= 0.5:
return "badge-amber"
return "badge-red"
# ββ Cached resource: heavy models loaded once per session βββββββββββββββββββββ
@st.cache_resource(show_spinner=False)
def load_pipeline():
"""
Load Phase 4 LangGraph pipeline.
Cached by Streamlit so models are only loaded once.
"""
from phase4_tools import build_phase4_components, build_phase4_graph
components = build_phase4_components()
app = build_phase4_graph(components)
return components, app
def run_query(query: str, components: dict, app) -> dict:
from phase3_agent import make_initial_state
state = make_initial_state(query)
result = app.invoke(state)
return result
def _parse_retry_seconds(error_msg: str) -> str:
"""Extract human-readable wait time from a Groq rate-limit error message."""
match = re.search(r"try again in ([\d]+m[\d.]+s|[\d.]+s|[\d]+m)", str(error_msg))
return match.group(1) if match else "a few minutes"
# ββ Sidebar βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
with st.sidebar:
st.markdown("## π FinAgent")
st.markdown("**Document:** Infosys Integrated Annual Report 2024-25")
st.divider()
st.markdown("### Pipeline")
st.markdown(
"- π **Hybrid retrieval** β BM25 + Dense (HyDE)\n"
"- π **Cross-encoder reranking** β top-4 chunks\n"
"- π€ **LangGraph agents** β intent-routed\n"
"- πΉ **FinBERT** β domain sentiment\n"
"- β»οΈ **Self-RAG** β inline faithfulness check"
)
st.divider()
st.markdown("### Agent Intents")
for icon, label, desc in [
("π", "Financial Metrics", "Revenue, margins, FCF, RoE"),
("βοΈ", "Compliance Check", "SEBI, risk factors, ESG"),
("π", "Investment Alert", "Signals with FinBERT sentiment"),
("π
", "Analyst Review", "Schedule review meetings"),
("π", "General Query", "Free-form document Q&A"),
]:
st.markdown(f"**{icon} {label}** β {desc}")
st.divider()
st.caption("Powered by Groq Β· LangGraph Β· ChromaDB Β· HuggingFace")
# ββ Main header βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
st.markdown('<p class="main-title">π FinAgent</p>', unsafe_allow_html=True)
st.markdown(
'<p class="sub-title">Ask anything about the <b>Infosys Annual Report 2024-25</b></p>',
unsafe_allow_html=True,
)
# ββ Example queries βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
st.markdown("##### Try an example")
example_cols = st.columns(3)
example_queries = [
"What was Infosys revenue and operating margin in FY25?",
"Are there any SEBI compliance issues mentioned in the annual report?",
"Generate an investment alert for Infosys based on FY25 performance.",
"What is Infosys AI strategy and Topaz platform?",
"What were the major risk factors disclosed by Infosys?",
"Schedule an analyst review for Infosys credit assessment.",
]
# The query lives in session_state so example clicks persist across reruns
# (otherwise clicking an example then "Ask" submits an empty box).
if "query_text" not in st.session_state:
st.session_state.query_text = ""
for i, col in enumerate(example_cols):
with col:
if st.button(f"π¬ {example_queries[i*2][:45]}β¦", key=f"ex_{i*2}", use_container_width=True):
st.session_state.query_text = example_queries[i * 2]
if st.button(f"π¬ {example_queries[i*2+1][:45]}β¦", key=f"ex_{i*2+1}", use_container_width=True):
st.session_state.query_text = example_queries[i * 2 + 1]
st.divider()
# ββ Query input βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Bound to session_state via key β the widget owns the value, so example
# clicks above pre-fill it and the text survives the "Ask" rerun.
query = st.text_area(
"Enter your question",
key="query_text",
height=80,
placeholder="e.g. What was Infosys revenue in FY25?",
label_visibility="collapsed",
)
col_btn, col_clear = st.columns([1, 6])
with col_btn:
submit = st.button("π Ask FinAgent", type="primary", use_container_width=True)
with col_clear:
st.empty()
# ββ Pipeline loader βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if "pipeline_ready" not in st.session_state:
st.session_state.pipeline_ready = False
if not st.session_state.pipeline_ready:
with st.status("βοΈ Loading models (first run only β takes ~30s)β¦", expanded=True) as status:
st.write("Loading embedding model (all-mpnet-base-v2)β¦")
st.write("Connecting to ChromaDB vector storeβ¦")
st.write("Loading FinBERT sentiment modelβ¦")
components, app = load_pipeline()
st.session_state.pipeline_ready = True
status.update(label="β
Pipeline ready", state="complete", expanded=False)
else:
components, app = load_pipeline()
# ββ Run query βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if submit and query.strip():
with st.spinner("π€ Retrieving and reasoningβ¦"):
t0 = time.time()
try:
result = run_query(query.strip(), components, app)
except Exception as e:
err_str = str(e)
if "rate_limit_exceeded" in err_str or "429" in err_str:
wait = _parse_retry_seconds(err_str)
st.error(
f"β³ **Groq daily token limit reached.**\n\n"
f"Your free-tier quota (100,000 tokens/day) is exhausted. "
f"Please try again in **{wait}** β the limit resets at midnight UTC.\n\n"
f"**To avoid this:** upgrade to Groq Dev Tier at "
f"https://console.groq.com/settings/billing"
)
else:
st.error(f"β Unexpected error: {err_str}")
st.stop()
elapsed = round(time.time() - t0, 1)
# ββ Extract fields ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
answer = result.get("final_answer", "No answer generated.")
intent = result.get("intent", "")
faith_score = result.get("faithfulness_score", 0.0) or 0.0
citations = result.get("citations", [])
doc_dicts = result.get("retrieved_docs", [])
tool_name = result.get("tool_name", "")
tool_output = result.get("tool_output", {})
iteration = result.get("iteration_count", 0)
# ββ Top metrics row βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
m1, m2, m3, m4 = st.columns(4)
m1.metric("π― Intent", intent.replace("_", " ").title() if intent else "β")
m2.metric("β‘ Latency", f"{elapsed}s")
m3.metric("β
Faithfulness", f"{faith_score:.0%}")
m4.metric("π Self-RAG iters", f"{iteration + 1}")
st.divider()
# ββ Answer ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
st.markdown("### π‘ Answer")
st.markdown(
f'<div class="answer-box">{answer}</div>',
unsafe_allow_html=True,
)
# ββ Citations βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if citations:
chips = " ".join(
f'<span class="cite-chip">π Page {p}</span>'
for p in sorted(set(citations))
if p is not None
)
st.markdown(f"**Citations:** {chips}", unsafe_allow_html=True)
st.divider()
# ββ Retrieved context βββββββββββββββββββββββββββββββββββββββββββββββββββββ
if doc_dicts:
with st.expander(f"π Retrieved context ({len(doc_dicts)} chunks)", expanded=False):
for i, doc in enumerate(doc_dicts, 1):
page = doc.get("metadata", {}).get("page", "?")
text = doc.get("page_content", "").strip()
st.markdown(
f'<div class="context-box">'
f'<b>Chunk {i} β Page {page}</b><br>{text[:600]}'
f'{"β¦" if len(text) > 600 else ""}'
f"</div>",
unsafe_allow_html=True,
)
# ββ Tool output βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if tool_output and tool_name:
with st.expander(f"π§ Tool output β `{tool_name}`", expanded=False):
st.json(tool_output)
# ββ FinBERT sentiment (if investment alert) βββββββββββββββββββββββββββββββ
finbert = None
if isinstance(tool_output, dict):
finbert = tool_output.get("finbert_sentiment")
if finbert:
with st.expander("πΉ FinBERT Sentiment Breakdown", expanded=False):
label = finbert.get("label", "neutral").capitalize()
score = finbert.get("score", 0.0)
scores = finbert.get("all_scores", {})
fcol1, fcol2 = st.columns([1, 2])
with fcol1:
colour = {"Positive": "π’", "Negative": "π΄", "Neutral": "π‘"}.get(label, "βͺ")
st.metric(f"{colour} Dominant sentiment", f"{label} ({score:.0%})")
with fcol2:
if scores:
for sent, val in scores.items():
st.progress(val, text=f"{sent.capitalize()}: {val:.1%}")
elif submit and not query.strip():
st.warning("Please enter a question before clicking Ask.")
# ββ Footer ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
st.divider()
st.caption(
"FinAgent Β· RAG + LangGraph + FinBERT Β· "
"Groq llama-3.3-70b-versatile Β· ChromaDB 3,607 chunks Β· "
"Infosys AR 2024-25"
)
|