CoolFace
Apppublic

Anushka-stack-queues/FinBench

sourceHugging Faceupdated 6mo agoView on Hugging Face
0likes
client.py302 linesDownload Raw Back to root
1# Copyright (c) Meta Platforms, Inc. and affiliates.2# All rights reserved.3#4# This source code is licensed under the BSD-style license found in the5# LICENSE file in the root directory of this source tree.6 7"""Finbench Environment Client."""8 9from typing import Dict10 11from openenv.core import EnvClient12from openenv.core.client_types import StepResult13from openenv.core.env_server.types import State14 15from .models import FinbenchAction, FinbenchObservation16 17 18class FinbenchEnv(19    EnvClient[FinbenchAction, FinbenchObservation, State]20):21    """22    Client for the FinBench Financial Advisor Environment.23 24    This client maintains a persistent WebSocket connection to the environment server,25    enabling efficient multi-step interactions with lower latency.26    Each client instance has its own dedicated environment session on the server.27 28    Example:29        >>> with FinbenchEnv(base_url="http://localhost:8000") as client:30        ...     result = client.reset()31        ...     result = client.step(FinbenchAction(32        ...         action_type="allocate",33        ...         allocations={"equities": 20, "bonds": 55, "cash": 15, "real_estate": 5, "commodities": 5},34        ...         reasoning="Conservative client near retirement."35        ...     ))36        ...     print(result.observation.feedback)37    """38 39    def _step_payload(self, action: FinbenchAction) -> Dict:40        """Convert FinbenchAction to JSON payload for step message."""41        return action.model_dump()42 43    def _parse_result(self, payload: Dict) -> StepResult[FinbenchObservation]:44        """Parse server response into StepResult[FinbenchObservation]."""45        obs_data = payload.get("observation", {})46        observation = FinbenchObservation(47            client=obs_data.get("client", {}),48            market_conditions=obs_data.get("market_conditions", {}),49            task_id=obs_data.get("task_id", ""),50            task_description=obs_data.get("task_description", ""),51            step_number=obs_data.get("step_number", 0),52            previous_actions=obs_data.get("previous_actions", []),53            feedback=obs_data.get("feedback", ""),54            done=payload.get("done", False),55            reward=payload.get("reward"),56            metadata=obs_data.get("metadata", {}),57        )58        return StepResult(59            observation=observation,60            reward=payload.get("reward"),61            done=payload.get("done", False),62        )63 64    def _parse_state(self, payload: Dict) -> State:65        """Parse server response into State object."""66        return State(67            episode_id=payload.get("episode_id"),68            step_count=payload.get("step_count", 0),69        )70 71 72# ── Rule-based demo agents ────────────────────────────────────────────────────73 74def rule_based_agent_task1(obs: FinbenchObservation) -> FinbenchAction:75    """Simple rule-based agent for portfolio allocation."""76    client = obs.client77    rt = client["risk_tolerance"]78    age = client["age"]79    has_emergency_fund = client["has_emergency_fund"]80 81    profiles = {82        "conservative": {"equities": 18, "bonds": 57, "cash": 15, "real_estate": 5, "commodities": 5},83        "moderate": {"equities": 50, "bonds": 35, "cash": 8, "real_estate": 4, "commodities": 3},84        "aggressive": {"equities": 70, "bonds": 12, "cash": 5, "real_estate": 8, "commodities": 5},85    }86    allocs = profiles[rt].copy()87 88    if age >= 60 and rt != "conservative":89        allocs["bonds"] += 1090        allocs["equities"] -= 1091 92    if not has_emergency_fund:93        allocs["cash"] = max(allocs["cash"], 15)94        diff = sum(allocs.values()) - 10095        allocs["equities"] -= diff96 97    return FinbenchAction(98        action_type="allocate",99        allocations=allocs,100        reasoning=(101            f"Client is {age} years old with {rt} risk tolerance. "102            f"Adjusted allocation for age and emergency fund status."103        ),104    )105 106 107def rule_based_agent_task2(obs: FinbenchObservation) -> FinbenchAction:108    """Simple rule-based agent for risk assessment."""109    client = obs.client110    risks = []111    recs = []112    score = {113        "conservative": 3.0,114        "moderate": 5.0,115        "aggressive": 6.0,116    }[client["risk_tolerance"]]117 118    if not client["has_emergency_fund"]:119        risks.append("No emergency fund - financially vulnerable to unexpected expenses")120        recs.append("Build a 3-6 month emergency fund in a high-yield savings account")121        score += 1.5122 123    if not client["has_insurance"] and client["dependents"] > 0:124        risks.append(f"No insurance with {client['dependents']} dependent(s) - major risk")125        recs.append("Obtain term life insurance immediately to protect dependents")126        score += 2.0127 128    if client["debt_to_income_ratio"] > 0.36:129        risks.append(f"High debt-to-income ratio ({client['debt_to_income_ratio']:.0%}) above 36% threshold")130        recs.append("Implement debt avalanche strategy - pay highest-interest debt first")131        score += 1.5132 133    monthly = client["annual_income"] / 12134    savings_rate = (monthly - client["monthly_expenses"]) / monthly if monthly > 0 else 0135    if savings_rate < 0.15:136        risks.append(f"Low savings rate (~{savings_rate*100:.0f}%) - below recommended 15%")137        recs.append("Reduce discretionary spending to achieve at least 15% savings rate")138        score += 1.0139 140    for asset, pct in client.get("existing_portfolio", {}).items():141        if pct > 60:142            risks.append(f"Over-concentration in {asset} ({pct}%) - lacks diversification")143            recs.append(f"Rebalance away from {asset} to diversify risk")144            score += 1.0145 146    if client["tax_bracket"] >= 0.32:147        risks.append("High income with potential tax inefficiency")148        recs.append("Maximize tax-advantaged accounts: 401k, IRA, HSA")149        score += 0.5150 151    if not risks:152        risks.append("Client profile appears generally sound with few major risk flags")153        recs.append("Continue current strategy with annual review")154 155    return FinbenchAction(156        action_type="assess_risk",157        identified_risks=risks,158        risk_score=round(min(10.0, score), 1),159        recommendations=recs,160        priority_recommendation=recs[0] if recs else "Review financial plan annually.",161    )162 163 164def rule_based_agent_task3(obs: FinbenchObservation) -> FinbenchAction:165    """Simple rule-based agent for comprehensive financial planning."""166    client = obs.client167    monthly = client["annual_income"] / 12168    rt = client["risk_tolerance"]169 170    alloc_map = {171        "conservative": {"equities": 20, "bonds": 55, "cash": 15, "real_estate": 5, "commodities": 5},172        "moderate": {"equities": 50, "bonds": 32, "cash": 8, "real_estate": 7, "commodities": 3},173        "aggressive": {"equities": 68, "bonds": 15, "cash": 7, "real_estate": 7, "commodities": 3},174    }175 176    insurance = []177    if not client["has_insurance"]:178        insurance.append("Term life insurance - $500,000 coverage")179        if client["dependents"] > 0:180            insurance.append("Disability insurance to protect income")181 182    debt_strategy = ""183    if client["debt_to_income_ratio"] > 0.36:184        debt_strategy = (185            "Avalanche method: list all debts by interest rate, "186            "pay minimums on all but highest-rate debt, apply all extra cash there. "187            "Target DTI below 0.36 within 24-36 months."188        )189 190    tax_strategies = ["Maximize 401k contribution ($23,000/year)"]191    if client["tax_bracket"] >= 0.24:192        tax_strategies.append("Open and fund Roth IRA ($7,000/year)")193    if client["tax_bracket"] >= 0.32:194        tax_strategies.append("HSA contributions for triple tax advantage")195        tax_strategies.append("Tax-loss harvesting in taxable accounts")196        tax_strategies.append("Consider municipal bonds for tax-efficient fixed income")197 198    goal_timelines = {}199    for goal in client.get("goals", []):200        if "retire" in goal.lower():201            goal_timelines[goal] = max(65 - client["age"], 1)202        elif "home" in goal.lower() or "house" in goal.lower():203            goal_timelines[goal] = 5204        elif "education" in goal.lower() or "college" in goal.lower():205            goal_timelines[goal] = 18206        else:207            goal_timelines[goal] = 10208 209    return FinbenchAction(210        action_type="financial_plan",211        emergency_fund_months=6.0 if client["dependents"] > 0 else 3.0,212        insurance_recommendations=insurance,213        debt_payoff_strategy=debt_strategy,214        investment_allocations=alloc_map[rt],215        retirement_monthly_savings=round(max(monthly * 0.15, 500), 2),216        tax_optimization_strategies=tax_strategies,217        goal_timelines=goal_timelines,218        rebalancing_frequency="quarterly",219        reasoning=(220            f"Comprehensive plan for {client['age']}-year-old {rt} investor. "221            f"Annual income ${client['annual_income']:,.0f}, net worth ${client['net_worth']:,.0f}. "222            f"Priority order: emergency fund -> insurance -> debt -> invest -> tax optimize."223        ),224    )225 226 227# ── Demo runner ───────────────────────────────────────────────────────────────228 229AGENTS = {230    "task1_allocation": rule_based_agent_task1,231    "task2_risk": rule_based_agent_task2,232    "task3_plan": rule_based_agent_task3,233}234 235TASK_LABELS = {236    "task1_allocation": "Task 1 - Portfolio Allocation    [EASY]",237    "task2_risk": "Task 2 - Risk Assessment         [MEDIUM]",238    "task3_plan": "Task 3 - Comprehensive Plan      [HARD]",239}240 241 242def run_demo(base_url: str = "http://localhost:8000"):243    from .server.FinBench_environment import FinbenchEnvironment244 245    print("\n" + "=" * 60)246    print("  FinBench - Rule-Based Agent Demo")247    print("=" * 60)248 249    all_scores = []250 251    for task_id, agent_fn in AGENTS.items():252        print(f"\n{'-' * 60}")253        print(f"  {TASK_LABELS[task_id]}")254        print(f"{'-' * 60}")255 256        task_scores = []257        for scenario_idx in range(3):258            env = FinbenchEnvironment(task_id=task_id, scenario_index=scenario_idx)259            obs = env.reset()260            client = obs.client261 262            print(f"\n  Scenario {scenario_idx + 1}: {client.get('client_id')}")263            print(264                f"  Age={client.get('age')} | Risk={client.get('risk_tolerance')} | "265                f"Income=${client.get('annual_income'):,.0f} | DTI={client.get('debt_to_income_ratio'):.2f}"266            )267 268            best_reward = 0.0269            done = False270            step = 0271 272            while not done:273                action = agent_fn(obs)274                obs = env.step(action)275                done = obs.done276                reward = obs.reward or 0.0277                best_reward = max(best_reward, reward)278                step += 1279                print(f"  Step {step}: reward={reward:.4f}")280 281            task_scores.append(best_reward)282            print(f"  -> Best: {best_reward:.4f}")283 284        avg = sum(task_scores) / len(task_scores)285        all_scores.append(avg)286        print(f"\n  Task Average: {avg:.4f}")287 288    print(f"\n{'=' * 60}")289    print("  SUMMARY")290    print(f"{'=' * 60}")291    for task_id, score in zip(AGENTS.keys(), all_scores):292        bar = "#" * int(score * 20) + "." * (20 - int(score * 20))293        print(f"  {TASK_LABELS[task_id]}: {bar} {score:.4f}")294 295    overall = sum(all_scores) / len(all_scores)296    print(f"\n  Overall Average: {overall:.4f}")297    print("\n  Environment working correctly!\n")298 299 300if __name__ == "__main__":301    run_demo()302