diff --git a/examples/loan_covenant_monitoring/README.md b/examples/loan_covenant_monitoring/README.md new file mode 100644 index 0000000..f7173f8 --- /dev/null +++ b/examples/loan_covenant_monitoring/README.md @@ -0,0 +1,137 @@ +# Loan Covenant Monitoring Agent + +A multi-agent compliance monitoring system built with the **Upsonic AI Agent Framework**. This example demonstrates how to use `Team` in coordinate mode with specialized `Agent` instances and custom financial calculation tools to automate end-to-end loan covenant compliance analysis. + +## Features + +- 🏦 **Automated Covenant Extraction**: Parses loan agreements to identify all financial covenant definitions, thresholds, and constraint types +- 📊 **Financial Ratio Calculation**: Uses six custom calculation tools to compute leverage, interest coverage, current ratio, DSCR, and tangible net worth +- ⚠️ **Compliance Assessment**: Evaluates each covenant as compliant, near-breach, or breached with headroom percentages +- 🛡️ **Risk Scoring**: Produces an overall risk score (0-100) with actionable remediation recommendations +- 🏗️ **Modular Design**: Clean separation of concerns with specialized agents, schemas, tools, and team configuration +- 🧠 **Structured Output**: Returns a comprehensive `CovenantMonitoringReport` Pydantic model with full audit trail + +## Prerequisites + +- Python 3.10+ +- Anthropic API key +- OpenAI API key + +## Installation + +1. **Navigate to this directory**: + ```bash + cd examples/loan_covenant_monitoring + ``` + +2. **Install dependencies**: + ```bash + # Install all dependencies (API) + upsonic install + + # Or install specific sections: + upsonic install api # API dependencies only (default) + upsonic install development # Development dependencies only + ``` + +3. **Set up environment variables**: + ```bash + export ANTHROPIC_API_KEY="your-api-key" + export OPENAI_API_KEY="your-api-key" + ``` + +## Usage + +### Run the API Server + +To run the agent as a FastAPI server: + +```bash +upsonic run +``` + +The API will be available at `http://localhost:8000` with automatic OpenAPI documentation at `http://localhost:8000/docs`. + +OR + +You can run the agent directly: + +```bash +uv run main.py +``` + +**Example API Call:** +```bash +curl -X POST http://localhost:8000/call \ + -H "Content-Type: application/json" \ + -d '{ + "inputs": { + "company_name": "GlobalTech Manufacturing Inc.", + "reporting_period": "Q4 2025", + "loan_agreement_path": "data/loan_agreement.txt", + "financial_data_path": "data/financial_data.json" + } + }' +``` + +## Project Structure + +``` +loan_covenant_monitoring/ +├── main.py # Entry point with async main() function +├── team.py # Team assembly (coordinate mode + leader) +├── agents.py # 3 specialist agent factory functions +├── schemas.py # Pydantic output schemas +├── tools.py # Custom financial calculation tools +├── task_builder.py # Task description builder +├── upsonic_configs.json # Upsonic configuration and dependencies +├── data/ +│ ├── loan_agreement.txt # Synthetic loan agreement (5 covenants) +│ └── financial_data.json # Synthetic Q4 2025 financials +└── README.md # This file +``` + +## How It Works + +1. **Team Assembly**: A `Team` in coordinate mode is created with a leader agent that orchestrates three specialist agents: Covenant Extractor, Financial Calculator, and Risk Assessor. + +2. **Covenant Extraction**: The Covenant Extractor agent parses the loan agreement document to identify every financial covenant, its threshold, formula, constraint type, and testing frequency. + +3. **Financial Calculation**: The Financial Calculator agent uses custom tools (`calculate_leverage_ratio`, `calculate_interest_coverage_ratio`, etc.) to compute all required ratios from raw financial data. + +4. **Compliance Evaluation**: The Risk Assessor agent uses the `evaluate_covenant_compliance` tool to determine each covenant's status (compliant, near-breach, or breached) and computes an overall risk score. + +5. **Structured Output**: The final response is enforced to match the `CovenantMonitoringReport` Pydantic model, ensuring consistent and machine-readable output with full audit trail. + +## Example Queries + +- Monitor Q4 2025 covenant compliance for GlobalTech Manufacturing Inc. +- Evaluate leverage ratio trends approaching covenant limits +- Assess debt service capacity under current cash flow conditions +- Identify breached covenants and recommend remediation strategies + +## Input Parameters + +- `company_name` (required): Name of the borrower company (e.g., "GlobalTech Manufacturing Inc.") +- `reporting_period` (required): Period being monitored (e.g., "Q4 2025") +- `loan_agreement_path` (required): Path to the loan agreement text file +- `financial_data_path` (required): Path to the financial data JSON file +- `focus_areas` (optional): List of priority focus areas for the analysis +- `enable_memory` (optional): Whether to enable in-memory session persistence (default: true) +- `model` (optional): Model identifier (default: "anthropic/claude-sonnet-4-5") + +## Output + +Returns a dictionary containing: +- `company_name`: The borrower company name +- `reporting_period`: The period that was monitored +- `report`: Full `CovenantMonitoringReport` object containing: + - `covenants_extracted`: All covenant definitions from the agreement + - `calculated_ratios`: Computed ratios with audit trail (formula, components) + - `compliance_results`: Per-covenant status with headroom percentages + - `risk_assessment`: Overall score (0-100), risk level, key concerns, and recommended actions + - `executive_summary`: Narrative summary of findings + - `next_steps`: Actionable remediation steps +- `monitoring_completed`: Whether the process completed successfully + +The report is returned as a JSON-serializable dictionary with all findings structured according to the `CovenantMonitoringReport` schema. diff --git a/examples/loan_covenant_monitoring/agents.py b/examples/loan_covenant_monitoring/agents.py new file mode 100644 index 0000000..161c94d --- /dev/null +++ b/examples/loan_covenant_monitoring/agents.py @@ -0,0 +1,162 @@ +""" +Specialized agent creation functions for covenant monitoring. + +Each function creates an Agent with a specific domain expertise: +- Covenant extraction from legal documents +- Financial ratio calculation using custom tools +- Compliance assessment and risk evaluation +""" + +from __future__ import annotations + +from typing import List, Callable + +from upsonic import Agent + +try: + from .tools import ( + calculate_leverage_ratio, + calculate_interest_coverage_ratio, + calculate_current_ratio, + calculate_debt_service_coverage_ratio, + calculate_tangible_net_worth, + evaluate_covenant_compliance, + ) +except ImportError: + from tools import ( + calculate_leverage_ratio, + calculate_interest_coverage_ratio, + calculate_current_ratio, + calculate_debt_service_coverage_ratio, + calculate_tangible_net_worth, + evaluate_covenant_compliance, + ) + + +def create_covenant_extractor_agent(model: str = "openai/gpt-4o-mini") -> Agent: + """Create a specialist agent for extracting covenant definitions from loan agreements. + + Args: + model: Model identifier for the agent. + + Returns: + Configured Agent for covenant extraction. + """ + return Agent( + model=model, + name="Covenant Extractor", + role="Legal Document Analyst specializing in commercial loan agreements", + goal=( + "Extract and structure every financial covenant definition from the loan " + "agreement, including precise thresholds, formulas, constraint types, and " + "testing frequencies" + ), + system_prompt=( + "You are a specialist in analyzing commercial loan agreements and credit " + "facility documentation. Your task is to:\n" + "- Identify every financial covenant in the provided agreement text\n" + "- Extract the exact numerical threshold for the applicable period\n" + "- Determine the formula specified for each covenant\n" + "- Classify each as 'maximum' (must not exceed) or 'minimum' (must not fall below)\n" + "- Note the testing frequency (quarterly TTM, point-in-time, etc.)\n\n" + "CRITICAL: Only extract values explicitly stated in the document. Never infer " + "or estimate thresholds. Use the exact step-down schedule applicable to the " + "reporting period being analyzed." + ), + education="JD in Corporate Law, CFA Charterholder", + work_experience="15 years in leveraged finance documentation and loan agreement analysis", + tool_call_limit=5, + ) + + +def create_financial_calculator_agent(model: str = "openai/gpt-4o-mini") -> Agent: + """Create a specialist agent for computing financial ratios using calculation tools. + + Args: + model: Model identifier for the agent. + + Returns: + Configured Agent with financial calculation tools. + """ + financial_tools: List[Callable[..., dict]] = [ + calculate_leverage_ratio, + calculate_interest_coverage_ratio, + calculate_current_ratio, + calculate_debt_service_coverage_ratio, + calculate_tangible_net_worth, + ] + + return Agent( + model=model, + name="Financial Calculator", + role="Quantitative Financial Analyst", + goal=( + "Calculate all required financial ratios and metrics from raw financial " + "data using the provided calculation tools, producing an audit-ready trail" + ), + system_prompt=( + "You are a quantitative analyst responsible for computing financial ratios " + "needed for covenant compliance testing.\n\n" + "RULES:\n" + "1. ALWAYS use the provided calculation tools. NEVER compute ratios manually.\n" + "2. Use exact figures from the financial data. Do not round or adjust inputs.\n" + "3. For each ratio, identify the correct input values from the financial data " + "and call the corresponding tool.\n" + "4. Report all results with their component values for audit trail.\n\n" + "Available tools:\n" + "- calculate_leverage_ratio(total_debt, ebitda)\n" + "- calculate_interest_coverage_ratio(ebit, interest_expense)\n" + "- calculate_current_ratio(current_assets, current_liabilities)\n" + "- calculate_debt_service_coverage_ratio(net_operating_income, total_debt_service)\n" + "- calculate_tangible_net_worth(total_assets, total_liabilities, intangible_assets)" + ), + education="MS in Financial Engineering, FRM Certification", + work_experience="10 years in credit risk analytics and financial modeling", + tools=financial_tools, + tool_call_limit=15, + ) + + +def create_risk_assessor_agent(model: str = "openai/gpt-4o-mini") -> Agent: + """Create a specialist agent for evaluating covenant compliance and risk. + + Args: + model: Model identifier for the agent. + + Returns: + Configured Agent with the compliance evaluation tool. + """ + compliance_tools: List[Callable[..., dict]] = [evaluate_covenant_compliance] + + return Agent( + model=model, + name="Risk Assessor", + role="Credit Risk Officer", + goal=( + "Evaluate covenant compliance status for each covenant using the evaluation " + "tool, calculate overall risk score, and provide actionable recommendations" + ), + system_prompt=( + "You are a senior credit risk officer evaluating loan covenant compliance.\n\n" + "PROCESS:\n" + "1. For each covenant, call evaluate_covenant_compliance with:\n" + " - covenant_name: the covenant's name\n" + " - actual_value: the calculated ratio/metric value\n" + " - threshold: the covenant threshold from the agreement\n" + " - constraint_type: 'maximum' or 'minimum'\n" + "2. Analyze headroom percentage for each to assess comfort level\n" + "3. Compute overall risk score using this methodology:\n" + " - Start at 0 (no risk)\n" + " - Add 30 points per breached covenant\n" + " - Add 15 points per near-breach covenant\n" + " - Risk levels: 0-20=Low, 21-40=Moderate, 41-70=High, 71-100=Critical\n" + "4. Provide specific, actionable recommendations for any covenant that is " + "near breach or breached, considering both immediate remediation and " + "structural solutions\n\n" + "Consider cure provisions and grace periods when formulating recommendations." + ), + education="MBA in Finance, PRM Certification", + work_experience="12 years in commercial banking credit risk and portfolio monitoring", + tools=compliance_tools, + tool_call_limit=15, + ) diff --git a/examples/loan_covenant_monitoring/data/financial_data.json b/examples/loan_covenant_monitoring/data/financial_data.json new file mode 100644 index 0000000..892593a --- /dev/null +++ b/examples/loan_covenant_monitoring/data/financial_data.json @@ -0,0 +1,92 @@ +{ + "company_name": "GlobalTechMac Manufacturing Inc.", + "reporting_period": "Q4 2025", + "period_end_date": "2025-12-31", + "measurement_basis": "Trailing Twelve Months (TTM) ending December 31, 2025", + "currency": "USD", + + "income_statement_ttm": { + "revenue": 285000000, + "cost_of_goods_sold": 192000000, + "gross_profit": 93000000, + "selling_general_administrative": 51000000, + "depreciation_expense": 13500000, + "amortization_expense": 4500000, + "total_operating_expenses": 69000000, + "operating_income_ebit": 24000000, + "interest_expense": 8600000, + "pre_tax_income": 15400000, + "income_tax_expense": 3850000, + "net_income": 11550000, + "ebitda": 42000000, + "ebitda_adjustments_note": "EBITDA includes $2,100,000 add-back for non-recurring restructuring charges approved by Lender per Section 7.1" + }, + + "balance_sheet_q4_2025": { + "current_assets": { + "cash_and_equivalents": 12500000, + "accounts_receivable_net": 35800000, + "inventory": 24500000, + "prepaid_expenses": 5500000, + "total_current_assets": 78300000 + }, + "non_current_assets": { + "property_plant_equipment_net": 175000000, + "goodwill": 22000000, + "patents_and_trademarks": 8500000, + "other_intangible_assets": 4500000, + "other_non_current_assets": 6200000, + "total_non_current_assets": 216200000 + }, + "total_assets": 294500000, + + "current_liabilities": { + "accounts_payable": 22000000, + "accrued_expenses": 12500000, + "current_portion_long_term_debt": 15000000, + "other_current_liabilities": 8500000, + "total_current_liabilities": 58000000 + }, + "non_current_liabilities": { + "long_term_debt": 120000000, + "capital_lease_obligations": 7500000, + "other_non_current_liabilities": 4000000, + "total_non_current_liabilities": 131500000 + }, + "total_liabilities": 189500000, + + "shareholders_equity": { + "common_stock_and_apic": 15000000, + "retained_earnings": 90000000, + "total_shareholders_equity": 105000000 + } + }, + + "debt_schedule": { + "senior_term_loan_outstanding": 120000000, + "current_portion_term_loan": 15000000, + "capital_lease_obligations": 7500000, + "outstanding_letters_of_credit": 9500000, + "total_funded_debt": 152000000, + "weighted_average_interest_rate_percent": 5.65, + "next_principal_payment_date": "2026-03-31", + "next_principal_payment_amount": 3750000 + }, + + "cash_flow_data_ttm": { + "ebitda": 42000000, + "unfunded_capital_expenditures": 10000000, + "cash_taxes_paid": 4860000, + "net_operating_income_for_dscr": 27140000, + "scheduled_principal_payments": 15000000, + "total_interest_paid": 8600000, + "total_debt_service": 23600000 + }, + + "intangible_assets_summary": { + "goodwill": 22000000, + "patents_and_trademarks": 8500000, + "other_intangibles": 4500000, + "total_intangible_assets": 35000000 + } +} diff --git a/examples/loan_covenant_monitoring/data/loan_agreement.txt b/examples/loan_covenant_monitoring/data/loan_agreement.txt new file mode 100644 index 0000000..52cbc7b --- /dev/null +++ b/examples/loan_covenant_monitoring/data/loan_agreement.txt @@ -0,0 +1,164 @@ +SENIOR SECURED TERM LOAN AGREEMENT + +Effective Date: January 15, 2024 + +LENDER: Meridian Capital Bank, N.A. ("Lender") +BORROWER: GlobalTech Manufacturing Inc., a Delaware corporation ("Borrower") + +Facility Amount: $150,000,000 (One Hundred Fifty Million Dollars) +Maturity Date: January 15, 2029 +Interest Rate: SOFR + 275 basis points, adjusted quarterly +Amortization: $15,000,000 annual principal repayment, payable quarterly + +================================================================================ +ARTICLE VII — FINANCIAL COVENANTS +================================================================================ + +The Borrower shall maintain compliance with the following financial covenants, +tested quarterly on the last day of each fiscal quarter based on trailing +twelve-month ("TTM") figures unless otherwise specified. Compliance shall be +demonstrated via a Compliance Certificate delivered with each quarterly +financial statement package. + +-------------------------------------------------------------------------------- +Section 7.1 — Maximum Leverage Ratio +-------------------------------------------------------------------------------- + +The Borrower shall not permit the ratio of Total Funded Debt to EBITDA +(the "Leverage Ratio") to exceed: + + - 4.00 to 1.00 for fiscal quarters ending on or before December 31, 2024 + - 3.75 to 1.00 for fiscal quarters ending January 1, 2025 through December 31, 2025 + - 3.50 to 1.00 for fiscal quarters ending on or after January 1, 2026 + +Testing frequency: Quarterly, on a TTM basis. + +Definitions for this covenant: + "Total Funded Debt" means all indebtedness for borrowed money, including + the outstanding principal balance of term loans, revolving credit facilities, + capital lease obligations, and the face amount of all outstanding letters + of credit. + + "EBITDA" means net income plus (i) interest expense, (ii) income tax expense, + (iii) depreciation expense, and (iv) amortization expense, in each case + determined on a consolidated basis in accordance with GAAP, and adjusted to + exclude the impact of non-recurring charges approved by the Lender. + +-------------------------------------------------------------------------------- +Section 7.2 — Minimum Interest Coverage Ratio +-------------------------------------------------------------------------------- + +The Borrower shall maintain a ratio of EBIT to Interest Expense +(the "Interest Coverage Ratio") of not less than 2.00 to 1.00, tested +quarterly on a TTM basis. + +Testing frequency: Quarterly, on a TTM basis. + +Definitions for this covenant: + "EBIT" means net income plus (i) interest expense and (ii) income tax + expense, determined on a consolidated basis in accordance with GAAP. + + "Interest Expense" means all interest paid or accrued on Total Funded Debt + during the applicable measurement period, including commitment fees and + letter of credit fees, but excluding amortization of deferred financing costs. + +-------------------------------------------------------------------------------- +Section 7.3 — Minimum Current Ratio +-------------------------------------------------------------------------------- + +The Borrower shall maintain a ratio of Current Assets to Current Liabilities +(the "Current Ratio") of not less than 1.20 to 1.00, tested as of the last +day of each fiscal quarter on a point-in-time basis. + +Testing frequency: Quarterly, point-in-time (balance sheet date). + +Definitions for this covenant: + "Current Assets" means all assets classified as current in accordance with + GAAP, including cash and cash equivalents, accounts receivable, inventory, + and prepaid expenses. + + "Current Liabilities" means all liabilities classified as current in + accordance with GAAP, including accounts payable, accrued expenses, the + current portion of long-term debt, and other short-term obligations. + +-------------------------------------------------------------------------------- +Section 7.4 — Minimum Debt Service Coverage Ratio +-------------------------------------------------------------------------------- + +The Borrower shall maintain a ratio of Net Operating Income to Total Debt +Service (the "Debt Service Coverage Ratio" or "DSCR") of not less than +1.25 to 1.00, tested quarterly on a TTM basis. + +Testing frequency: Quarterly, on a TTM basis. + +Definitions for this covenant: + "Net Operating Income" means EBITDA less (i) unfunded capital expenditures + (capital expenditures not financed with new debt) and (ii) cash taxes paid + during the applicable period. + + "Total Debt Service" means the sum of (i) all scheduled principal payments + and (ii) all interest payments on Total Funded Debt during the applicable + measurement period. + +-------------------------------------------------------------------------------- +Section 7.5 — Minimum Tangible Net Worth +-------------------------------------------------------------------------------- + +The Borrower shall maintain Tangible Net Worth of not less than $50,000,000 +(Fifty Million Dollars), tested as of the last day of each fiscal quarter on +a point-in-time basis. + +Testing frequency: Quarterly, point-in-time (balance sheet date). + +Definitions for this covenant: + "Tangible Net Worth" means total assets minus (i) total liabilities and + minus (ii) all intangible assets, including goodwill, patents, trademarks, + copyrights, organizational costs, and other intangible assets as classified + under GAAP. + +================================================================================ +ARTICLE VIII — CURE PROVISIONS +================================================================================ + +Section 8.1 — Equity Cure Right + +In the event of a breach of any financial covenant set forth in Article VII, +the Borrower shall have the right to cure such breach by contributing +additional equity capital within thirty (30) days of the delivery of the +Compliance Certificate demonstrating such breach. Such equity cure right may +be exercised no more than two (2) times during the term of this Agreement and +not in consecutive fiscal quarters. Equity cure proceeds shall be added to +EBITDA solely for the purpose of recalculating the breached covenant for the +applicable measurement period. + +Section 8.2 — Notice and Grace Period + +Upon discovery of any covenant breach, the Borrower shall deliver written +notice to the Lender within five (5) business days. A grace period of fifteen +(15) business days shall apply before a breach constitutes an Event of Default +under Article X, provided the Borrower is actively pursuing commercially +reasonable remediation steps. + +================================================================================ +ARTICLE IX — REPORTING REQUIREMENTS +================================================================================ + +Section 9.1 — Quarterly Compliance Certificate + +Within forty-five (45) days after the end of each fiscal quarter, the +Borrower shall deliver to the Lender a Compliance Certificate signed by the +Chief Financial Officer, certifying compliance with all financial covenants +set forth in Article VII and including detailed calculations for each covenant. + +Section 9.2 — Annual Audited Financial Statements + +Within ninety (90) days after the end of each fiscal year, the Borrower shall +deliver audited consolidated financial statements prepared in accordance with +GAAP by an independent accounting firm of national recognition. + +Section 9.3 — Prompt Notice of Material Events + +The Borrower shall promptly notify the Lender of any event that could +reasonably be expected to result in a breach of any financial covenant, +any pending or threatened litigation in excess of $5,000,000, or any material +adverse change in the Borrower's financial condition or business operations. diff --git a/examples/loan_covenant_monitoring/main.py b/examples/loan_covenant_monitoring/main.py new file mode 100644 index 0000000..19f8dc9 --- /dev/null +++ b/examples/loan_covenant_monitoring/main.py @@ -0,0 +1,181 @@ +""" +Main entry point for Loan Covenant Monitoring Agent. + +This module provides the async entry point "main" that coordinates +the comprehensive loan covenant monitoring process. +""" + +from __future__ import annotations + +import asyncio +import json +from pathlib import Path +from typing import Dict, Any, Optional, List + +from upsonic import Task + +try: + from .team import create_covenant_monitoring_team + from .task_builder import build_covenant_monitoring_task + from .schemas import CovenantMonitoringReport +except ImportError: + from team import create_covenant_monitoring_team + from task_builder import build_covenant_monitoring_task + from schemas import CovenantMonitoringReport + + +async def main(inputs: Dict[str, Any]) -> Dict[str, Any]: + """ + Main async function for loan covenant monitoring. + + Args: + inputs: Dictionary containing: + - company_name: Name of the borrower company (required) + - reporting_period: Period being monitored, e.g. "Q4 2025" (required) + - loan_agreement_path: Path to loan agreement text file (required) + - financial_data_path: Path to financial data JSON file (required) + - focus_areas: Optional list of priority focus areas + - enable_memory: Whether to enable memory persistence (default: True) + - model: Optional model identifier (default: "anthropic/claude-sonnet-4-5") + - print: If True, do() prints; if False, print_do() does not. Respects UPSONIC_AGENT_PRINT env. + + Returns: + Dictionary containing the covenant monitoring report and metadata. + """ + company_name: str = inputs.get("company_name", "") + if not company_name: + raise ValueError("company_name is required in inputs") + + reporting_period: str = inputs.get("reporting_period", "") + if not reporting_period: + raise ValueError("reporting_period is required in inputs") + + loan_agreement_path: str = inputs.get("loan_agreement_path", "") + if not loan_agreement_path: + raise ValueError("loan_agreement_path is required in inputs") + + financial_data_path: str = inputs.get("financial_data_path", "") + if not financial_data_path: + raise ValueError("financial_data_path is required in inputs") + + focus_areas: Optional[List[str]] = inputs.get("focus_areas") + enable_memory: bool = inputs.get("enable_memory", True) + model: str = inputs.get("model", "anthropic/claude-sonnet-4-5") + print_flag: Optional[bool] = inputs.get("print") + + team = create_covenant_monitoring_team( + model=model, + enable_memory=enable_memory, + print=print_flag, + ) + + task_description: str = build_covenant_monitoring_task( + company_name=company_name, + reporting_period=reporting_period, + focus_areas=focus_areas, + ) + + task: Task = Task( + task_description, + context=[loan_agreement_path, financial_data_path], + ) + + result = await team.do_async(task) + + if isinstance(result, CovenantMonitoringReport): + report_dict: Dict[str, Any] = result.model_dump() + else: + report_dict = {"raw_output": str(result)} + + return { + "company_name": company_name, + "reporting_period": reporting_period, + "report": report_dict, + "monitoring_completed": True, + } + + +if __name__ == "__main__": + import sys + + script_dir: Path = Path(__file__).parent + + test_inputs: Dict[str, Any] = { + "company_name": "GlobalTech Manufacturing Inc.", + "reporting_period": "Q4 2025", + "loan_agreement_path": str(script_dir / "data" / "loan_agreement.txt"), + "financial_data_path": str(script_dir / "data" / "financial_data.json"), + "focus_areas": [ + "Leverage ratio trending toward covenant limit", + "Debt service capacity under current cash flow", + ], + "enable_memory": False, + "model": "anthropic/claude-sonnet-4-5", + "print": True, + } + + if len(sys.argv) > 1: + try: + with open(sys.argv[1], "r") as f: + test_inputs = json.load(f) + except Exception as e: + print(f"Error loading JSON file: {e}") + print("Using default test inputs") + + try: + result = asyncio.run(main(test_inputs)) + + print("\n" + "=" * 80) + print("Loan Covenant Monitoring Report - Completed") + print("=" * 80) + print(f"\nCompany: {result['company_name']}") + print(f"Period: {result['reporting_period']}") + print(f"Status: {'Completed' if result.get('monitoring_completed') else 'Failed'}") + + report: Dict[str, Any] = result.get("report", {}) + + if "executive_summary" in report: + print(f"\n--- Executive Summary ---\n{report['executive_summary']}") + + if "risk_assessment" in report: + risk: Dict[str, Any] = report["risk_assessment"] + print("\n--- Risk Assessment ---") + print(f" Risk Score : {risk.get('overall_risk_score', 'N/A')} / 100") + print(f" Risk Level : {risk.get('risk_level', 'N/A')}") + print( + f" Breached: {risk.get('breached_count', 0)} | " + f"Near Breach: {risk.get('near_breach_count', 0)} | " + f"Compliant: {risk.get('compliant_count', 0)}" + ) + + if "compliance_results" in report: + print("\n--- Covenant Compliance Details ---") + print("-" * 60) + status_symbols: Dict[str, str] = { + "compliant": "[OK]", + "near_breach": "[!!]", + "breached": "[XX]", + } + for covenant in report["compliance_results"]: + symbol: str = status_symbols.get(covenant.get("status", ""), "[??]") + print( + f" {symbol} {covenant.get('covenant_name', 'Unknown')}: " + f"{covenant.get('actual_value', 'N/A')} vs " + f"{covenant.get('threshold', 'N/A')} " + f"(headroom: {covenant.get('headroom_percentage', 'N/A')}%)" + ) + + if "next_steps" in report: + print("\n--- Recommended Next Steps ---") + for i, step in enumerate(report["next_steps"], 1): + print(f" {i}. {step}") + + print("\n" + "=" * 80) + print("\nFull report (JSON):") + print(json.dumps(report, indent=2, default=str)) + + except Exception as e: + print(f"\nError during execution: {e}") + import traceback + traceback.print_exc() + sys.exit(1) diff --git a/examples/loan_covenant_monitoring/schemas.py b/examples/loan_covenant_monitoring/schemas.py new file mode 100644 index 0000000..18dbf30 --- /dev/null +++ b/examples/loan_covenant_monitoring/schemas.py @@ -0,0 +1,65 @@ +""" +Output schemas for loan covenant monitoring agent. + +Defines structured Pydantic models for type-safe outputs from the +covenant monitoring pipeline. +""" + +from __future__ import annotations + +from typing import List +from pydantic import BaseModel, Field + + +class CovenantDefinition(BaseModel): + """A single financial covenant extracted from a loan agreement.""" + name: str + description: str + formula: str + threshold: float + constraint_type: str = Field(description="Either 'maximum' or 'minimum'") + testing_frequency: str + + +class FinancialRatio(BaseModel): + """A calculated financial ratio with audit trail.""" + name: str + value: float + formula_used: str + components: dict[str, float] + + +class CovenantComplianceResult(BaseModel): + """Compliance evaluation result for a single covenant.""" + covenant_name: str + threshold: float + actual_value: float + status: str = Field(description="One of: 'compliant', 'near_breach', 'breached'") + headroom_percentage: float = Field(description="Positive = buffer remaining, negative = extent of breach") + explanation: str + + +class RiskAssessment(BaseModel): + """Overall portfolio risk assessment across all covenants.""" + overall_risk_score: float = Field(ge=0.0, le=100.0) + risk_level: str = Field(description="One of: 'low', 'moderate', 'high', 'critical'") + total_covenants: int + compliant_count: int + near_breach_count: int + breached_count: int + key_concerns: List[str] + recommended_actions: List[str] + + +class CovenantMonitoringReport(BaseModel): + """Final comprehensive covenant monitoring report.""" + company_name: str + reporting_period: str + report_date: str + covenants_extracted: List[CovenantDefinition] + calculated_ratios: List[FinancialRatio] + compliance_results: List[CovenantComplianceResult] + risk_assessment: RiskAssessment + executive_summary: str + detailed_findings: str + next_steps: List[str] diff --git a/examples/loan_covenant_monitoring/task_builder.py b/examples/loan_covenant_monitoring/task_builder.py new file mode 100644 index 0000000..dde7ba2 --- /dev/null +++ b/examples/loan_covenant_monitoring/task_builder.py @@ -0,0 +1,55 @@ +""" +Task description builder for loan covenant monitoring. + +Constructs the instruction-only task description. Actual data +(loan agreement, financial data) is provided via Task.context. +""" + +from __future__ import annotations + +from typing import Optional, List + + +def build_covenant_monitoring_task( + company_name: str, + reporting_period: str, + focus_areas: Optional[List[str]] = None, +) -> str: + """Build the instruction task description for the covenant monitoring team. + + Args: + company_name: Name of the borrower company. + reporting_period: Period under analysis (e.g. "Q4 2025"). + focus_areas: Optional list of priority areas to emphasize. + + Returns: + Formatted task description string containing only instructions. + """ + focus_text: str = "" + if focus_areas: + focus_list: str = "\n".join([f" - {area}" for area in focus_areas]) + focus_text = f"\n\nPRIORITY FOCUS AREAS:\n{focus_list}" + + task_description: str = ( + f"Perform comprehensive covenant compliance monitoring for {company_name} " + f"for the reporting period {reporting_period}.\n\n" + f"You have been provided the full loan agreement document and financial data " + f"as context. Use them to complete the following deliverables:\n\n" + f"REQUIRED DELIVERABLES:\n" + f"1. Extract all financial covenants from the loan agreement, including " + f"names, formulas, thresholds applicable to {reporting_period}, constraint types " + f"(maximum or minimum), and testing frequencies\n" + f"2. Calculate all required financial ratios using the financial data and the " + f"calculation tools (do NOT compute manually)\n" + f"3. Evaluate compliance status for each covenant (compliant, near_breach, or " + f"breached) using the compliance evaluation tool\n" + f"4. Produce an overall risk assessment with a numerical risk score (0-100) " + f"and risk level\n" + f"5. Write a concise executive summary highlighting any breaches or near-breaches " + f"and their business implications\n" + f"6. Provide specific, actionable next steps for remediation where needed, " + f"referencing cure provisions from the agreement if applicable" + f"{focus_text}" + ) + + return task_description diff --git a/examples/loan_covenant_monitoring/team.py b/examples/loan_covenant_monitoring/team.py new file mode 100644 index 0000000..9b1b988 --- /dev/null +++ b/examples/loan_covenant_monitoring/team.py @@ -0,0 +1,99 @@ +""" +Team assembly and configuration for covenant monitoring. + +Creates a coordinated Team with a leader agent that orchestrates +covenant extraction, financial calculation, and compliance assessment. +""" + +from __future__ import annotations + +from typing import Optional + +from upsonic import Agent, Team +from upsonic.storage import Memory, InMemoryStorage + +try: + from .agents import ( + create_covenant_extractor_agent, + create_financial_calculator_agent, + create_risk_assessor_agent, + ) + from .schemas import CovenantMonitoringReport +except ImportError: + from agents import ( + create_covenant_extractor_agent, + create_financial_calculator_agent, + create_risk_assessor_agent, + ) + from schemas import CovenantMonitoringReport + + +def create_covenant_monitoring_team( + model: str = "anthropic/claude-sonnet-4-5", + enable_memory: bool = True, + print: Optional[bool] = None, +) -> Team: + """Create the coordinated Team for end-to-end covenant monitoring. + + Uses coordinate mode with a leader agent that delegates to three + specialist agents: covenant extractor, financial calculator, and + risk assessor. + + Args: + model: Model identifier for the leader/coordinator agent. + enable_memory: Whether to enable in-memory session persistence. + print: If True, do() prints; if False, print_do() does not. Respects UPSONIC_AGENT_PRINT env. + + Returns: + Configured Team instance ready for covenant monitoring tasks. + """ + leader: Agent = Agent( + model=model, + name="Covenant Monitoring Coordinator", + role="Head of Loan Portfolio Monitoring", + goal=( + "Coordinate the end-to-end covenant monitoring process by delegating " + "to specialist agents and synthesizing a comprehensive compliance report" + ), + system_prompt=( + "You coordinate the loan covenant monitoring workflow.\n\n" + "WORKFLOW:\n" + "1. Delegate to Covenant Extractor: Have them parse the loan agreement and " + "extract all covenant definitions with thresholds for the applicable period\n" + "2. Delegate to Financial Calculator: Have them calculate all required ratios " + "using the financial data and their calculation tools\n" + "3. Delegate to Risk Assessor: Have them evaluate compliance for each covenant " + "using the extracted thresholds and calculated ratios\n" + "4. Synthesize all findings into the final structured report\n\n" + "IMPORTANT:\n" + "- Ensure each covenant definition is matched with its corresponding ratio\n" + "- Pass the correct threshold and constraint type to the risk assessor\n" + "- The final report must cover every covenant's compliance status\n" + "- Include an overall risk assessment with a numerical score and risk level\n" + "- Provide actionable next steps, especially for any breached or near-breach covenants" + ), + ) + + memory: Optional[Memory] = None + if enable_memory: + memory = Memory( + storage=InMemoryStorage(), + session_id="covenant_monitoring_session", + full_session_memory=True, + ) + + covenant_extractor: Agent = create_covenant_extractor_agent() + financial_calculator: Agent = create_financial_calculator_agent() + risk_assessor: Agent = create_risk_assessor_agent() + + team: Team = Team( + entities=[covenant_extractor, financial_calculator, risk_assessor], + mode="coordinate", + leader=leader, + response_format=CovenantMonitoringReport, + memory=memory, + name="Loan Covenant Monitoring Team", + print=print, + ) + + return team diff --git a/examples/loan_covenant_monitoring/tools.py b/examples/loan_covenant_monitoring/tools.py new file mode 100644 index 0000000..726d940 --- /dev/null +++ b/examples/loan_covenant_monitoring/tools.py @@ -0,0 +1,217 @@ +""" +Custom financial calculation tools for covenant monitoring. + +Provides standalone calculation and compliance evaluation functions +used by the financial calculator and risk assessor agents. +""" + +from __future__ import annotations + +from typing import Dict, Any + + +def calculate_leverage_ratio(total_debt: float, ebitda: float) -> Dict[str, Any]: + """Calculate the Leverage Ratio (Total Debt / EBITDA). + + Args: + total_debt: Total outstanding funded debt in dollars. + ebitda: Earnings Before Interest, Taxes, Depreciation, and Amortization in dollars. + + Returns: + Dictionary with ratio value, formula used, and input components. + """ + if ebitda <= 0: + return { + "ratio_name": "Leverage Ratio", + "value": float("inf"), + "formula": "Total Funded Debt / EBITDA", + "components": {"total_debt": total_debt, "ebitda": ebitda}, + "warning": "EBITDA is zero or negative; ratio is undefined", + } + + ratio: float = round(total_debt / ebitda, 4) + return { + "ratio_name": "Leverage Ratio", + "value": ratio, + "formula": "Total Funded Debt / EBITDA", + "components": {"total_debt": total_debt, "ebitda": ebitda}, + } + + +def calculate_interest_coverage_ratio(ebit: float, interest_expense: float) -> Dict[str, Any]: + """Calculate the Interest Coverage Ratio (EBIT / Interest Expense). + + Args: + ebit: Earnings Before Interest and Taxes in dollars. + interest_expense: Total interest expense in dollars. + + Returns: + Dictionary with ratio value, formula used, and input components. + """ + if interest_expense <= 0: + return { + "ratio_name": "Interest Coverage Ratio", + "value": float("inf"), + "formula": "EBIT / Interest Expense", + "components": {"ebit": ebit, "interest_expense": interest_expense}, + "warning": "Interest expense is zero or negative; ratio is undefined", + } + + ratio: float = round(ebit / interest_expense, 4) + return { + "ratio_name": "Interest Coverage Ratio", + "value": ratio, + "formula": "EBIT / Interest Expense", + "components": {"ebit": ebit, "interest_expense": interest_expense}, + } + + +def calculate_current_ratio(current_assets: float, current_liabilities: float) -> Dict[str, Any]: + """Calculate the Current Ratio (Current Assets / Current Liabilities). + + Args: + current_assets: Total current assets in dollars. + current_liabilities: Total current liabilities in dollars. + + Returns: + Dictionary with ratio value, formula used, and input components. + """ + if current_liabilities <= 0: + return { + "ratio_name": "Current Ratio", + "value": float("inf"), + "formula": "Current Assets / Current Liabilities", + "components": {"current_assets": current_assets, "current_liabilities": current_liabilities}, + "warning": "Current liabilities is zero or negative; ratio is undefined", + } + + ratio: float = round(current_assets / current_liabilities, 4) + return { + "ratio_name": "Current Ratio", + "value": ratio, + "formula": "Current Assets / Current Liabilities", + "components": {"current_assets": current_assets, "current_liabilities": current_liabilities}, + } + + +def calculate_debt_service_coverage_ratio( + net_operating_income: float, + total_debt_service: float, +) -> Dict[str, Any]: + """Calculate the Debt Service Coverage Ratio (Net Operating Income / Total Debt Service). + + Args: + net_operating_income: EBITDA minus unfunded capex minus cash taxes paid, in dollars. + total_debt_service: Sum of scheduled principal payments and interest payments, in dollars. + + Returns: + Dictionary with ratio value, formula used, and input components. + """ + if total_debt_service <= 0: + return { + "ratio_name": "Debt Service Coverage Ratio", + "value": float("inf"), + "formula": "Net Operating Income / Total Debt Service", + "components": { + "net_operating_income": net_operating_income, + "total_debt_service": total_debt_service, + }, + "warning": "Total debt service is zero or negative; ratio is undefined", + } + + ratio: float = round(net_operating_income / total_debt_service, 4) + return { + "ratio_name": "Debt Service Coverage Ratio", + "value": ratio, + "formula": "Net Operating Income / Total Debt Service", + "components": { + "net_operating_income": net_operating_income, + "total_debt_service": total_debt_service, + }, + } + + +def calculate_tangible_net_worth( + total_assets: float, + total_liabilities: float, + intangible_assets: float, +) -> Dict[str, Any]: + """Calculate Tangible Net Worth (Total Assets - Total Liabilities - Intangible Assets). + + Args: + total_assets: Total assets in dollars. + total_liabilities: Total liabilities in dollars. + intangible_assets: Intangible assets including goodwill, patents, trademarks, in dollars. + + Returns: + Dictionary with calculated value, formula used, and input components. + """ + tangible_net_worth: float = round(total_assets - total_liabilities - intangible_assets, 2) + return { + "metric_name": "Tangible Net Worth", + "value": tangible_net_worth, + "formula": "Total Assets - Total Liabilities - Intangible Assets", + "components": { + "total_assets": total_assets, + "total_liabilities": total_liabilities, + "intangible_assets": intangible_assets, + }, + } + + +def evaluate_covenant_compliance( + covenant_name: str, + actual_value: float, + threshold: float, + constraint_type: str, +) -> Dict[str, Any]: + """Evaluate whether a financial covenant is compliant, near breach, or breached. + + Uses a 10 percent buffer zone to determine near-breach status. + + Args: + covenant_name: Name of the covenant being evaluated. + actual_value: The actual calculated ratio or metric value. + threshold: The covenant threshold from the loan agreement. + constraint_type: Either 'maximum' (value must be at or below threshold) or 'minimum' (value must be at or above threshold). + + Returns: + Dictionary with compliance status, headroom percentage, and assessment details. + """ + near_breach_buffer: float = 0.10 + + if constraint_type.lower() not in ("maximum", "minimum"): + return { + "covenant_name": covenant_name, + "error": f"Invalid constraint_type '{constraint_type}'. Must be 'maximum' or 'minimum'.", + } + + if constraint_type.lower() == "maximum": + if actual_value > threshold: + status: str = "breached" + headroom_pct: float = round(-((actual_value - threshold) / threshold) * 100, 2) + elif actual_value > threshold * (1 - near_breach_buffer): + status = "near_breach" + headroom_pct = round(((threshold - actual_value) / threshold) * 100, 2) + else: + status = "compliant" + headroom_pct = round(((threshold - actual_value) / threshold) * 100, 2) + else: + if actual_value < threshold: + status = "breached" + headroom_pct = round(-((threshold - actual_value) / threshold) * 100, 2) + elif actual_value < threshold * (1 + near_breach_buffer): + status = "near_breach" + headroom_pct = round(((actual_value - threshold) / threshold) * 100, 2) + else: + status = "compliant" + headroom_pct = round(((actual_value - threshold) / threshold) * 100, 2) + + return { + "covenant_name": covenant_name, + "actual_value": actual_value, + "threshold": threshold, + "constraint_type": constraint_type, + "status": status, + "headroom_percentage": headroom_pct, + } diff --git a/examples/loan_covenant_monitoring/upsonic_configs.json b/examples/loan_covenant_monitoring/upsonic_configs.json new file mode 100644 index 0000000..298ab20 --- /dev/null +++ b/examples/loan_covenant_monitoring/upsonic_configs.json @@ -0,0 +1,116 @@ +{ + "envinroment_variables": { + "UPSONIC_WORKERS_AMOUNT": { + "type": "number", + "description": "The number of workers for the Upsonic API", + "default": 1 + }, + "API_WORKERS": { + "type": "number", + "description": "The number of workers for the Upsonic API", + "default": 1 + }, + "RUNNER_CONCURRENCY": { + "type": "number", + "description": "The number of runners for the Upsonic API", + "default": 1 + } + }, + "machine_spec": { + "cpu": 2, + "memory": 4096, + "storage": 1024 + }, + "agent_name": "Loan Covenant Monitoring Agent", + "description": "AI agent team that monitors loan covenant compliance by extracting covenant definitions from agreements, calculating financial ratios with custom tools, and assessing breach risk using coordinated specialist agents", + "icon": "shield-check", + "language": "python", + "streamlit": false, + "proxy_agent": false, + "dependencies": { + "api": [ + "upsonic", + "anthropic" + ], + "development": [ + "python-dotenv", + "pytest" + ] + }, + "entrypoints": { + "api_file": "main.py", + "streamlit_file": "streamlit_app.py" + }, + "input_schema": { + "inputs": { + "company_name": { + "type": "string", + "description": "Name of the borrower company (required)", + "required": true, + "default": null + }, + "reporting_period": { + "type": "string", + "description": "Period being monitored, e.g. 'Q4 2025' (required)", + "required": true, + "default": null + }, + "loan_agreement_path": { + "type": "string", + "description": "Path to the loan agreement text file (required)", + "required": true, + "default": null + }, + "financial_data_path": { + "type": "string", + "description": "Path to the financial data JSON file (required)", + "required": true, + "default": null + }, + "focus_areas": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Optional list of priority focus areas for the analysis", + "required": false, + "default": null + }, + "enable_memory": { + "type": "boolean", + "description": "Whether to enable in-memory session persistence", + "required": false, + "default": true + }, + "model": { + "type": "string", + "description": "Model identifier for the coordinator agent (e.g. anthropic/claude-sonnet-4-5, openai/gpt-4o)", + "required": false, + "default": "anthropic/claude-sonnet-4-5" + }, + "print": { + "type": "boolean", + "description": "If true, do() prints output; if false, print_do() does not. Overridden by UPSONIC_AGENT_PRINT env.", + "required": false + } + } + }, + "output_schema": { + "company_name": { + "type": "string", + "description": "The borrower company name" + }, + "reporting_period": { + "type": "string", + "description": "The period that was monitored" + }, + "report": { + "type": "object", + "description": "Full CovenantMonitoringReport with covenants, ratios, compliance results, risk assessment, executive summary, and next steps" + }, + "monitoring_completed": { + "type": "boolean", + "description": "Whether the monitoring process completed successfully" + } + } +} diff --git a/pyproject.toml b/pyproject.toml index cc60364..53d1ec2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,7 @@ dependencies = [ "ipdb>=0.13.13", "pip>=25.3", "ddgs>=9.10.0", + "anthropic>=0.83.0", ] [tool.uv] diff --git a/uv.lock b/uv.lock index 596628f..b57ba29 100644 --- a/uv.lock +++ b/uv.lock @@ -201,6 +201,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] +[[package]] +name = "anthropic" +version = "0.83.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "docstring-parser" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/e5/02cd2919ec327b24234abb73082e6ab84c451182cc3cc60681af700f4c63/anthropic-0.83.0.tar.gz", hash = "sha256:a8732c68b41869266c3034541a31a29d8be0f8cd0a714f9edce3128b351eceb4", size = 534058, upload-time = "2026-02-19T19:26:38.904Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/75/b9d58e4e2a4b1fc3e75ffbab978f999baf8b7c4ba9f96e60edb918ba386b/anthropic-0.83.0-py3-none-any.whl", hash = "sha256:f069ef508c73b8f9152e8850830d92bd5ef185645dbacf234bb213344a274810", size = 456991, upload-time = "2026-02-19T19:26:40.114Z" }, +] + [[package]] name = "anyio" version = "4.11.0" @@ -976,6 +995,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, ] +[[package]] +name = "docstring-parser" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, +] + [[package]] name = "duckduckgo-search" version = "8.1.1" @@ -1016,6 +1044,7 @@ dependencies = [ { name = "aiofiles" }, { name = "aiohttp" }, { name = "aiosqlite" }, + { name = "anthropic" }, { name = "asyncpg" }, { name = "chromadb" }, { name = "ddgs" }, @@ -1052,6 +1081,7 @@ requires-dist = [ { name = "aiofiles", specifier = ">=24.1.0" }, { name = "aiohttp", specifier = ">=3.12.15" }, { name = "aiosqlite", specifier = ">=0.21.0" }, + { name = "anthropic", specifier = ">=0.83.0" }, { name = "asyncpg", specifier = ">=0.30.0" }, { name = "chromadb", specifier = ">=1.3.7" }, { name = "ddgs", specifier = ">=9.10.0" },