mirror of
https://github.com/Shubhamsaboo/awesome-llm-apps.git
synced 2026-04-29 14:48:44 -05:00
Merge pull request #354 from gargigupta97/gargigupta97-patch-1
Add AI Life Insurance Advisor Agent app
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
# 🛡️ Life Insurance Coverage Advisor
|
||||
|
||||
A Streamlit application that helps users estimate the amount of term life insurance they may need and surfaces currently available policy options. The app is powered by the [Agno](https://github.com/agno-agi/agno) agent framework, uses **Grok 4 Fast (via OpenRouter)** as the reasoning model, the **E2B** sandbox for deterministic coverage calculations, and **Firecrawl** for live web research.
|
||||
|
||||
## Highlights
|
||||
- Minimal intake form (age, income, dependents, debt, assets, existing cover, horizon, location).
|
||||
- The agent runs Python code inside an E2B sandbox to calculate coverage with a discounted cash-flow style income replacement model.
|
||||
- Firecrawl search is used to gather the latest term-life products for the user’s geography and coverage needs.
|
||||
- Returns a concise coverage estimate, calculation breakdown, and up to three product suggestions with source links.
|
||||
|
||||
## Prerequisites
|
||||
You will need API keys for each external service:
|
||||
|
||||
| Service | Purpose | Where to get it |
|
||||
| --- | --- | --- |
|
||||
| OpenRouter (Grok 4 Fast) | Core reasoning model | https://openrouter.ai/keys |
|
||||
| Firecrawl | Web search + crawl tooling | https://www.firecrawl.dev/app/api-keys |
|
||||
| E2B | Secure code execution sandbox | https://e2b.dev |
|
||||
|
||||
> ℹ️ OpenRouter recommends setting the `HTTP-Referer` and `X-Title` headers on every request. The app defaults to
|
||||
> `https://github.com/Shubhamsaboo/awesome-llm-apps` and `Life Insurance Coverage Advisor`, but you can override them
|
||||
> by exporting `OPENROUTER_REFERRER` and `OPENROUTER_TITLE` before launching Streamlit.
|
||||
|
||||
## Installation
|
||||
1. Clone the GitHub repository
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Shubhamsaboo/awesome-llm-apps.git
|
||||
```
|
||||
2. Create and activate a virtual environment (optional but recommended).
|
||||
|
||||
3. Install dependencies:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
4. Run the Streamlit app:
|
||||
```bash
|
||||
streamlit run life_insurance_advisor.py
|
||||
```
|
||||
|
||||
## Using the App
|
||||
1. Enter your OpenRouter, Firecrawl, and E2B API keys in the sidebar (keys are kept in the local Streamlit session).
|
||||
2. Provide the requested financial information and choose an income replacement horizon.
|
||||
3. Click **Generate Coverage & Options** to launch the Agno agent workflow.
|
||||
4. Review the recommended coverage, rationale, and suggested insurers. Raw agent output is available in an expander for debugging.
|
||||
|
||||
## Disclaimer
|
||||
This project is for educational and prototyping purposes only and does **not** provide licensed financial advice. Always validate the output with a qualified professional and confirm details directly with insurance providers.
|
||||
@@ -0,0 +1,427 @@
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import streamlit as st
|
||||
from agno.agent import Agent
|
||||
from agno.models.openrouter import OpenRouter
|
||||
from agno.tools.e2b import E2BTools
|
||||
from agno.tools.firecrawl import FirecrawlTools
|
||||
|
||||
st.set_page_config(
|
||||
page_title="Life Insurance Coverage Advisor",
|
||||
page_icon="🛡️",
|
||||
layout="centered",
|
||||
)
|
||||
|
||||
st.title("🛡️ Life Insurance Coverage Advisor")
|
||||
st.caption(
|
||||
"Prototype Streamlit app powered by Agno Agents, Grok 4 Fast via OpenRouter, E2B sandboxed code execution, and Firecrawl search."
|
||||
)
|
||||
|
||||
DEFAULT_OPENROUTER_REFERRER = os.getenv(
|
||||
"OPENROUTER_REFERRER",
|
||||
"https://github.com/Shubhamsaboo/awesome-llm-apps",
|
||||
)
|
||||
DEFAULT_OPENROUTER_TITLE = os.getenv(
|
||||
"OPENROUTER_TITLE",
|
||||
"Life Insurance Coverage Advisor",
|
||||
)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Sidebar configuration for API keys
|
||||
# -----------------------------------------------------------------------------
|
||||
with st.sidebar:
|
||||
st.header("API Keys")
|
||||
st.write("All keys stay local in your browser session.")
|
||||
openrouter_api_key = st.text_input(
|
||||
"OpenRouter API Key",
|
||||
type="password",
|
||||
key="openrouter_api_key",
|
||||
help="Create one at https://openrouter.ai/keys",
|
||||
)
|
||||
firecrawl_api_key = st.text_input(
|
||||
"Firecrawl API Key",
|
||||
type="password",
|
||||
key="firecrawl_api_key",
|
||||
help="Create one at https://www.firecrawl.dev/app/api-keys",
|
||||
)
|
||||
e2b_api_key = st.text_input(
|
||||
"E2B API Key",
|
||||
type="password",
|
||||
key="e2b_api_key",
|
||||
help="Create one at https://e2b.dev",
|
||||
)
|
||||
st.markdown("---")
|
||||
st.caption(
|
||||
"The agent uses E2B for deterministic coverage math and Firecrawl for fresh term-life product research."
|
||||
)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Helper functions
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
def safe_number(value: Any) -> float:
|
||||
"""Best-effort conversion to float for agent outputs."""
|
||||
if value is None:
|
||||
return 0.0
|
||||
try:
|
||||
return float(value)
|
||||
except (TypeError, ValueError):
|
||||
if isinstance(value, str):
|
||||
stripped = value
|
||||
for token in [",", "$", "€", "£", "₹", "C$", "A$"]:
|
||||
stripped = stripped.replace(token, "")
|
||||
stripped = stripped.strip()
|
||||
try:
|
||||
return float(stripped)
|
||||
except ValueError:
|
||||
return 0.0
|
||||
return 0.0
|
||||
|
||||
|
||||
def format_currency(amount: float, currency_code: str) -> str:
|
||||
symbol_map = {
|
||||
"USD": "$",
|
||||
"EUR": "€",
|
||||
"GBP": "£",
|
||||
"CAD": "C$",
|
||||
"AUD": "A$",
|
||||
"INR": "₹",
|
||||
}
|
||||
code = (currency_code or "USD").upper()
|
||||
symbol = symbol_map.get(code, "")
|
||||
formatted = f"{amount:,.0f}"
|
||||
return f"{symbol}{formatted}" if symbol else f"{formatted} {code}"
|
||||
|
||||
|
||||
def extract_json(payload: str) -> Optional[Dict[str, Any]]:
|
||||
if not payload:
|
||||
return None
|
||||
|
||||
content = payload.strip()
|
||||
if content.startswith("```"):
|
||||
lines = content.splitlines()
|
||||
if lines[0].startswith("```"):
|
||||
lines = lines[1:]
|
||||
if lines and lines[-1].startswith("```"):
|
||||
lines = lines[:-1]
|
||||
content = "\n".join(lines).strip()
|
||||
|
||||
try:
|
||||
return json.loads(content)
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
|
||||
|
||||
def parse_percentage(value: Any, fallback: float = 0.02) -> float:
|
||||
"""Convert percentage-like values to decimal form (e.g., "2%" -> 0.02)."""
|
||||
if value is None:
|
||||
return fallback
|
||||
if isinstance(value, (int, float)):
|
||||
# assume already decimal if less than 1, otherwise treat as percentage value
|
||||
return float(value) if value < 1 else float(value) / 100
|
||||
if isinstance(value, str):
|
||||
cleaned = value.strip().replace("%", "")
|
||||
try:
|
||||
numeric = float(cleaned)
|
||||
return numeric if numeric < 1 else numeric / 100
|
||||
except ValueError:
|
||||
return fallback
|
||||
return fallback
|
||||
|
||||
|
||||
def compute_local_breakdown(profile: Dict[str, Any], real_rate: float) -> Dict[str, float]:
|
||||
"""Replicate the coverage math locally so we can show it to the user."""
|
||||
income = safe_number(profile.get("annual_income"))
|
||||
years = max(0, int(profile.get("income_replacement_years", 0) or 0))
|
||||
total_debt = safe_number(profile.get("total_debt"))
|
||||
savings = safe_number(profile.get("available_savings"))
|
||||
existing_cover = safe_number(profile.get("existing_life_insurance"))
|
||||
|
||||
if real_rate <= 0:
|
||||
discounted_income = income * years
|
||||
annuity_factor = years
|
||||
else:
|
||||
annuity_factor = (1 - (1 + real_rate) ** (-years)) / real_rate if years else 0
|
||||
discounted_income = income * annuity_factor
|
||||
|
||||
assets_offset = savings + existing_cover
|
||||
recommended = max(0.0, discounted_income + total_debt - assets_offset)
|
||||
|
||||
return {
|
||||
"income": income,
|
||||
"years": years,
|
||||
"real_rate": real_rate,
|
||||
"annuity_factor": annuity_factor,
|
||||
"discounted_income": discounted_income,
|
||||
"debt": total_debt,
|
||||
"assets_offset": -assets_offset,
|
||||
"recommended": recommended,
|
||||
}
|
||||
|
||||
|
||||
@st.cache_resource(show_spinner=False)
|
||||
def get_agent(openrouter_key: str, firecrawl_key: str, e2b_key: str) -> Optional[Agent]:
|
||||
if not (openrouter_key and firecrawl_key and e2b_key):
|
||||
return None
|
||||
|
||||
os.environ["OPENROUTER_API_KEY"] = openrouter_key
|
||||
os.environ["FIRECRAWL_API_KEY"] = firecrawl_key
|
||||
os.environ["E2B_API_KEY"] = e2b_key
|
||||
|
||||
return Agent(
|
||||
name="Life Insurance Advisor",
|
||||
model=OpenRouter(
|
||||
id="x-ai/grok-4-fast:free",
|
||||
api_key=openrouter_key,
|
||||
default_headers={
|
||||
"HTTP-Referer": DEFAULT_OPENROUTER_REFERRER,
|
||||
"X-Title": DEFAULT_OPENROUTER_TITLE,
|
||||
},
|
||||
),
|
||||
tools=[
|
||||
E2BTools(timeout=180),
|
||||
FirecrawlTools(
|
||||
api_key=firecrawl_key,
|
||||
enable_search=True,
|
||||
enable_crawl=True,
|
||||
enable_scrape=False,
|
||||
search_params={"limit": 5, "lang": "en"},
|
||||
),
|
||||
],
|
||||
instructions=[
|
||||
"You provide conservative life insurance guidance. Your workflow is strictly:",
|
||||
"1. ALWAYS call `run_python_code` from the E2B tools to compute the coverage recommendation using the provided client JSON.",
|
||||
" - Treat missing numeric values as 0.",
|
||||
" - Use a default real discount rate of 2% when discounting income replacement cash flows.",
|
||||
" - Compute: discounted_income = annual_income * ((1 - (1 + r)**(-income_replacement_years)) / r).",
|
||||
" - Recommended coverage = max(0, discounted_income + total_debt - savings - existing_life_insurance).",
|
||||
" - Print a JSON with keys: coverage_amount, coverage_currency, breakdown, assumptions.",
|
||||
"2. Use Firecrawl `search` followed by optional `scrape_website` calls to gather up-to-date term life insurance options for the client's region.",
|
||||
"3. Respond ONLY with JSON containing the following top-level keys: coverage_amount, coverage_currency, breakdown, assumptions, recommendations, research_notes, timestamp.",
|
||||
" - `coverage_amount`: integer of total recommended coverage.",
|
||||
" - `coverage_currency`: 3-letter currency code.",
|
||||
" - `breakdown`: include income_replacement, debt_obligations, assets_offset, methodology.",
|
||||
" - `assumptions`: include income_replacement_years, real_discount_rate, additional_notes.",
|
||||
" - `recommendations`: list of up to three objects (name, summary, link, source).",
|
||||
" - `research_notes`: brief disclaimer + recency of sources.",
|
||||
" - `timestamp`: ISO 8601 date-time string.",
|
||||
"Do not include markdown, commentary, or tool call traces in the final JSON output.",
|
||||
],
|
||||
markdown=False,
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# User input form
|
||||
# -----------------------------------------------------------------------------
|
||||
st.subheader("Tell us about yourself")
|
||||
|
||||
with st.form("coverage_form"):
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
age = st.number_input("Age", min_value=18, max_value=85, value=35)
|
||||
annual_income = st.number_input(
|
||||
"Annual Income",
|
||||
min_value=0.0,
|
||||
value=85000.0,
|
||||
step=1000.0,
|
||||
)
|
||||
dependents = st.number_input(
|
||||
"Dependents",
|
||||
min_value=0,
|
||||
max_value=10,
|
||||
value=2,
|
||||
step=1,
|
||||
)
|
||||
location = st.text_input(
|
||||
"Country / State",
|
||||
value="United States",
|
||||
help="Used to localize recommended insurers.",
|
||||
)
|
||||
with col2:
|
||||
total_debt = st.number_input(
|
||||
"Total Outstanding Debt (incl. mortgage)",
|
||||
min_value=0.0,
|
||||
value=200000.0,
|
||||
step=5000.0,
|
||||
)
|
||||
savings = st.number_input(
|
||||
"Savings & Investments available to dependents",
|
||||
min_value=0.0,
|
||||
value=50000.0,
|
||||
step=5000.0,
|
||||
)
|
||||
existing_cover = st.number_input(
|
||||
"Existing Life Insurance",
|
||||
min_value=0.0,
|
||||
value=100000.0,
|
||||
step=5000.0,
|
||||
)
|
||||
currency = st.selectbox(
|
||||
"Currency",
|
||||
options=["USD", "CAD", "EUR", "GBP", "AUD", "INR"],
|
||||
index=0,
|
||||
)
|
||||
|
||||
income_replacement_years = st.selectbox(
|
||||
"Income Replacement Horizon",
|
||||
options=[5, 10, 15],
|
||||
index=1,
|
||||
help="Number of years your income should be replaced for dependents.",
|
||||
)
|
||||
|
||||
submitted = st.form_submit_button("Generate Coverage & Options")
|
||||
|
||||
|
||||
def build_client_profile() -> Dict[str, Any]:
|
||||
return {
|
||||
"age": age,
|
||||
"annual_income": annual_income,
|
||||
"dependents": dependents,
|
||||
"location": location,
|
||||
"total_debt": total_debt,
|
||||
"available_savings": savings,
|
||||
"existing_life_insurance": existing_cover,
|
||||
"income_replacement_years": income_replacement_years,
|
||||
"currency": currency,
|
||||
"request_timestamp": datetime.utcnow().isoformat(),
|
||||
}
|
||||
|
||||
|
||||
def render_recommendations(result: Dict[str, Any], profile: Dict[str, Any]) -> None:
|
||||
coverage_currency = result.get("coverage_currency", currency)
|
||||
coverage_amount = safe_number(result.get("coverage_amount", 0))
|
||||
|
||||
st.subheader("Recommended Coverage")
|
||||
st.metric(
|
||||
label="Total Coverage Needed",
|
||||
value=format_currency(coverage_amount, coverage_currency),
|
||||
)
|
||||
|
||||
assumptions = result.get("assumptions", {})
|
||||
real_rate = parse_percentage(assumptions.get("real_discount_rate", "2%"))
|
||||
local_breakdown = compute_local_breakdown(profile, real_rate)
|
||||
|
||||
st.subheader("Calculation Inputs")
|
||||
st.table(
|
||||
{
|
||||
"Input": [
|
||||
"Annual income",
|
||||
"Income replacement horizon",
|
||||
"Total debt",
|
||||
"Liquid assets",
|
||||
"Existing life cover",
|
||||
"Real discount rate",
|
||||
],
|
||||
"Value": [
|
||||
format_currency(local_breakdown["income"], coverage_currency),
|
||||
f"{local_breakdown['years']} years",
|
||||
format_currency(local_breakdown["debt"], coverage_currency),
|
||||
format_currency(safe_number(profile.get("available_savings")), coverage_currency),
|
||||
format_currency(safe_number(profile.get("existing_life_insurance")), coverage_currency),
|
||||
f"{real_rate * 100:.2f}%",
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
st.subheader("Step-by-step Coverage Math")
|
||||
step_rows = [
|
||||
("Annuity factor", f"{local_breakdown['annuity_factor']:.3f}"),
|
||||
("Discounted income replacement", format_currency(local_breakdown["discounted_income"], coverage_currency)),
|
||||
("+ Outstanding debt", format_currency(local_breakdown["debt"], coverage_currency)),
|
||||
("- Assets & existing cover", format_currency(local_breakdown["assets_offset"], coverage_currency)),
|
||||
("= Formula estimate", format_currency(local_breakdown["recommended"], coverage_currency)),
|
||||
]
|
||||
step_rows.append(("= Agent recommendation", format_currency(coverage_amount, coverage_currency)))
|
||||
|
||||
st.table({"Step": [s for s, _ in step_rows], "Amount": [a for _, a in step_rows]})
|
||||
|
||||
breakdown = result.get("breakdown", {})
|
||||
with st.expander("How this number was calculated", expanded=True):
|
||||
st.markdown(
|
||||
f"- Income replacement value: {format_currency(safe_number(breakdown.get('income_replacement')), coverage_currency)}"
|
||||
)
|
||||
st.markdown(
|
||||
f"- Debt obligations: {format_currency(safe_number(breakdown.get('debt_obligations')), coverage_currency)}"
|
||||
)
|
||||
assets_offset = safe_number(breakdown.get("assets_offset"))
|
||||
st.markdown(
|
||||
f"- Assets & existing cover offset: {format_currency(assets_offset, coverage_currency)}"
|
||||
)
|
||||
methodology = breakdown.get("methodology")
|
||||
if methodology:
|
||||
st.caption(methodology)
|
||||
|
||||
recommendations = result.get("recommendations", [])
|
||||
if recommendations:
|
||||
st.subheader("Top Term Life Options")
|
||||
for idx, option in enumerate(recommendations, start=1):
|
||||
with st.container():
|
||||
name = option.get("name", "Unnamed Product")
|
||||
summary = option.get("summary", "No summary provided.")
|
||||
st.markdown(f"**{idx}. {name}** — {summary}")
|
||||
link = option.get("link")
|
||||
if link:
|
||||
st.markdown(f"[View details]({link})")
|
||||
source = option.get("source")
|
||||
if source:
|
||||
st.caption(f"Source: {source}")
|
||||
st.markdown("---")
|
||||
|
||||
with st.expander("Model assumptions"):
|
||||
st.write(
|
||||
{
|
||||
"Income replacement years": assumptions.get(
|
||||
"income_replacement_years", income_replacement_years
|
||||
),
|
||||
"Real discount rate": assumptions.get("real_discount_rate", "2%"),
|
||||
"Notes": assumptions.get("additional_notes", ""),
|
||||
}
|
||||
)
|
||||
|
||||
if result.get("research_notes"):
|
||||
st.caption(result["research_notes"])
|
||||
if result.get("timestamp"):
|
||||
st.caption(f"Generated: {result['timestamp']}")
|
||||
|
||||
with st.expander("Agent response JSON"):
|
||||
st.json(result)
|
||||
|
||||
|
||||
if submitted:
|
||||
if not all([openrouter_api_key, firecrawl_api_key, e2b_api_key]):
|
||||
st.error("Please configure OpenRouter, Firecrawl, and E2B API keys in the sidebar.")
|
||||
st.stop()
|
||||
|
||||
advisor_agent = get_agent(openrouter_api_key, firecrawl_api_key, e2b_api_key)
|
||||
if not advisor_agent:
|
||||
st.error("Unable to initialize the advisor. Double-check API keys.")
|
||||
st.stop()
|
||||
|
||||
client_profile = build_client_profile()
|
||||
user_prompt = (
|
||||
"You will receive a JSON object describing the client's profile. Follow your workflow instructions to calculate coverage and surface suitable products.\n"
|
||||
f"Client profile JSON: {json.dumps(client_profile)}"
|
||||
)
|
||||
|
||||
with st.spinner("Consulting advisor agent..."):
|
||||
response = advisor_agent.run(user_prompt, stream=False)
|
||||
|
||||
parsed = extract_json(response.content if response else "")
|
||||
if not parsed:
|
||||
st.error("The agent returned an unexpected response. Enable debug below to inspect raw output.")
|
||||
with st.expander("Raw agent output"):
|
||||
st.write(response.content if response else "<empty>")
|
||||
else:
|
||||
render_recommendations(parsed, client_profile)
|
||||
with st.expander("Agent debug"):
|
||||
st.write(response.content)
|
||||
|
||||
st.divider()
|
||||
st.caption(
|
||||
"This prototype is for educational use only and does not provide licensed financial advice. "
|
||||
"Verify all recommendations with a qualified professional and the insurers listed."
|
||||
)
|
||||
@@ -0,0 +1,5 @@
|
||||
streamlit>=1.32,<2.0
|
||||
agno>=1.1.7
|
||||
firecrawl-py>=1.9.0
|
||||
e2b-code-interpreter>=1.0.3
|
||||
openai>=1.30.0
|
||||
Reference in New Issue
Block a user