---
title: "Financial Tools and Structured Outputs"
url: "https://vanducng.dev/2026/01/12/Financial-Tools-and-Structured-Outputs/"
date: "2026-01-12"
series: "finance-ai"
categories: ["AI"]
tags: ["llm","python","finance"]
---

# Financial Tools and Structured Outputs

Language models are strong at reasoning, but without tools, they can only talk - not act. For financial applications requiring precise calculations, real-time data access, or system integrations, tool-augmented agents are essential. Combined with structured outputs using Pydantic, we can build reliable financial systems that guarantee data format compliance.

## From Talking to Acting: Tool-Augmented Agents

Consider a financial advisor agent. Without tools, it might attempt a rough ROI estimate based on training data. With tools, it can call a precise calculation function and return exact figures. The difference is critical in finance where accuracy matters.

```mermaid
flowchart TB
    subgraph WithTools["With Tools"]
        direction LR
        Q2[Calculate ROI?] --> T[Call: calculate_roi
inv=500000, profit=125000]
        T --> E[Execute Python]
        E --> R[25.00% exactly]
        R --> A2[Professional analysis
with exact figures]
    end

    subgraph NoTools["Without Tools"]
        direction LR
        Q1[Calculate ROI?] --> G1[Guess from
training data]
        G1 --> A1["~25% maybe?"]
    end

    classDef pinkClass fill:#E74C3C,stroke:#333,stroke-width:2px,color:#fff
    classDef greenClass fill:#27AE60,stroke:#333,stroke-width:2px,color:#fff

    class NoTools pinkClass
    class WithTools greenClass
```

### Common Tool Types for Finance

| Tool Type | Use Case | Example |
|-----------|----------|---------|
| **Math Functions** | Precise calculations | ROI, compound interest, risk metrics |
| **APIs & Webhooks** | External integrations | Market data, CRM, trading systems |
| **Databases** | Data retrieval | Position history, customer records |
| **Web Search** | Real-time information | News, regulatory updates |
| **Code Execution** | Custom transformations | Data processing, report generation |

## Function Calling: The Foundation

Early approaches used prompt-based cues like "Calling calculator with X and Y" followed by parsing. This was fragile. Modern function calling provides a formal mechanism where:

1. **Tools are defined** with JSON schemas describing name, purpose, and parameters
2. **Models are trained** to recognize when tools should be used
3. **Structured calls** are returned with function name and arguments
4. **Backend executes** the function and returns results to the model

### Building a Financial Tool Agent

Let's build an agent that can perform ROI and tax calculations:

```python
import json
from openai import OpenAI

client = OpenAI()

# System prompt for financial advisor persona
SYSTEM = """You are a helpful corporate financial advisor.
Provide clear, professional financial analysis and recommendations."""

# Tool 1: ROI Calculator
def calculate_roi(investment: float, profit: float, years: int) -> dict:
    """
    Calculate Return on Investment for corporate investments.
    Returns total and annualized ROI.
    """
    if investment <= 0:
        raise ValueError("Investment amount must be positive")

    total_roi = (profit / investment) * 100
    annual_roi = total_roi / years if years > 0 else 0

    return {
        "initial_investment": round(investment, 2),
        "profit": round(profit, 2),
        "total_roi_percent": round(total_roi, 2),
        "annualized_roi_percent": round(annual_roi, 2),
        "investment_period_years": years
    }

# Tool 2: Tax Deduction Calculator
def calculate_expense_deduction(
    gross_expenses: float,
    department: str,
    tax_rate: float = 0.21
) -> dict:
    """
    Calculate corporate tax deductions for departmental expenses.
    Different departments have different deduction rates.
    """
    DEDUCTION_RATES = {
        "operations": 0.95,
        "marketing": 0.80,
        "research": 0.90,
        "facilities": 0.75,
        "training": 0.85
    }

    dept_lower = department.lower()
    if dept_lower not in DEDUCTION_RATES:
        raise ValueError(f"Unknown department: {department}")

    deduction_rate = DEDUCTION_RATES[dept_lower]
    deductible_amount = gross_expenses * deduction_rate
    tax_savings = deductible_amount * tax_rate

    return {
        "gross_expenses": round(gross_expenses, 2),
        "department": department,
        "deduction_rate_percent": round(deduction_rate * 100, 1),
        "deductible_amount": round(deductible_amount, 2),
        "estimated_tax_savings": round(tax_savings, 2)
    }
```

### Defining Tool Schemas

Tools must be described in JSON schema format for the model to use them:

```python
TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "calculate_roi",
            "description": "Calculate Return on Investment for corporate investments.",
            "parameters": {
                "type": "object",
                "properties": {
                    "investment": {
                        "type": "number",
                        "description": "Initial investment amount in dollars"
                    },
                    "profit": {
                        "type": "number",
                        "description": "Total profit earned in dollars"
                    },
                    "years": {
                        "type": "integer",
                        "description": "Investment period in years"
                    }
                },
                "required": ["investment", "profit", "years"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "calculate_expense_deduction",
            "description": "Calculate corporate tax deductions for department expenses.",
            "parameters": {
                "type": "object",
                "properties": {
                    "gross_expenses": {
                        "type": "number",
                        "description": "Total gross expenses in dollars"
                    },
                    "department": {
                        "type": "string",
                        "description": "Department name (operations, marketing, research, etc.)"
                    },
                    "tax_rate": {
                        "type": "number",
                        "description": "Corporate tax rate (default 0.21)"
                    }
                },
                "required": ["gross_expenses", "department"]
            }
        }
    }
]

# Map tool names to Python callables
FUNCTIONS = {
    "calculate_roi": calculate_roi,
    "calculate_expense_deduction": calculate_expense_deduction
}
```

### The Tool Execution Flow

The complete flow involves three steps:

```mermaid
flowchart TD
    U[User: Analyze my $500K investment
with $125K profit over 3 years] --> M1[Model Call 1]
    M1 --> |tool_calls| TC[Tool Call: calculate_roi
investment=500000
profit=125000
years=3]
    TC --> EX[Execute Python Function]
    EX --> R[Result: total_roi=25%
annualized=8.33%]
    R --> M2[Model Call 2
with tool result]
    M2 --> A[Final Analysis:
Professional investment report
with exact figures]

    classDef blueClass fill:#4A90E2,stroke:#333,stroke-width:2px,color:#fff
    classDef orangeClass fill:#F39C12,stroke:#333,stroke-width:2px,color:#fff

    class TC blueClass
    class EX orangeClass
```

```python
def agentic_tool_call(tool_name: str, user_content: str, tool_args: dict = None):
    """Execute a tool call with agentic response generation."""

    # Step 1: Ask model to call the specified tool
    first = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": SYSTEM},
            {"role": "user", "content": user_content}
        ],
        tools=TOOLS,
        tool_choice={"type": "function", "function": {"name": tool_name}},
        temperature=0.0
    )

    msg = first.choices[0].message
    tool_call = msg.tool_calls[0] if msg.tool_calls else None

    if tool_call is None:
        raise RuntimeError("Model did not issue the expected tool call")

    # Step 2: Execute the Python function
    name = tool_call.function.name
    model_args = json.loads(tool_call.function.arguments or "{}")
    call_args = {**model_args, **(tool_args or {})}
    result = FUNCTIONS[name](**call_args)

    # Step 3: Send result back for final analysis
    messages = [
        {"role": "system", "content": SYSTEM},
        {"role": "user", "content": user_content},
        {"role": "assistant", "content": msg.content or "",
         "tool_calls": [tool_call.model_dump()]},
        {"role": "tool", "tool_call_id": tool_call.id,
         "name": name, "content": json.dumps(result)}
    ]

    final = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=messages,
        temperature=0.2
    )

    return final.choices[0].message.content
```

### Example Output

When called with an investment analysis request:

```python
result = agentic_tool_call(
    tool_name="calculate_roi",
    user_content="Analyze my company's $500,000 investment that earned $125,000 over 3 years."
)
```

The output provides exact figures with professional analysis:

```
Your company invested $500,000 and earned a profit of $125,000 over 3 years.

**ROI Analysis:**
1. **Total ROI**: 25.00% - Your investment has grown by a quarter
   of its original value over the period.
2. **Annualized ROI**: 8.33% - Average return per year, indicating
   steady growth.

**Recommendation**: A total ROI of 25% over 3 years is a positive
outcome. The annualized ROI of 8.33% is competitive compared to
traditional savings. Consider reinvesting profits to compound growth.
```

## Structured Outputs with Pydantic

Tools return data, but free-form LLM responses can be inconsistent. Structured outputs ensure machine-readable, validated data that downstream systems can rely on.

### The Problem with Naive JSON

Simply prompting "Return JSON" often fails:

```python
def naive_json_extraction():
    """Demonstrate problems with naive JSON parsing."""

    prompt = """
    Create an employee profile in JSON format with:
    - name: John Smith
    - employee_id: E123456
    Return only the JSON, no additional text.
    """

    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt}]
    )

    raw = response.choices[0].message.content
    # Model often returns: ```json\n{...}\n```
    # This breaks json.loads()

    try:
        return json.loads(raw)  # Fails!
    except json.JSONDecodeError as e:
        print(f"Parse failed: {e}")
```

The model might return markdown-wrapped JSON that breaks parsing.

### Pydantic to the Rescue

Pydantic provides type-safe data structures with validation:

```python
from pydantic import BaseModel, Field, ValidationError
from enum import Enum
from typing import List, Literal
from datetime import datetime

class EmployeeProfile(BaseModel):
    """Validated employee profile with type enforcement."""
    name: str = Field(..., min_length=1, max_length=100)
    employee_id: str = Field(..., pattern=r'^E\d{6}$')
    department: str = Field(..., min_length=1, max_length=50)
    email: str = Field(..., pattern=r'^[^@]+@[^@]+\.[^@]+$')
    tenure_years: int = Field(..., ge=0, le=60)

class ClaimType(str, Enum):
    MEDICAL = "medical"
    DENTAL = "dental"
    VISION = "vision"
    PREVENTIVE = "preventive"

class HealthClaim(BaseModel):
    """Individual health insurance claim."""
    claim_id: str = Field(..., pattern=r'^C\d{8}$')
    claim_type: ClaimType
    amount: float = Field(..., gt=0, le=1000000)
    description: str = Field(..., min_length=10, max_length=500)
    date_submitted: datetime
    status: Literal["pending", "approved", "denied"]

class ClaimsSummary(BaseModel):
    """Complete claims summary with nested validation."""
    employee: EmployeeProfile
    claims: List[HealthClaim]
    total_claims_amount: float
    approved_claims_count: int
    pending_claims_count: int
    average_claim_value: float
```

### Structured Output with OpenAI

Use `response_format={"type": "json_object"}` for guaranteed JSON:

```python
def generate_employee_profile_structured() -> EmployeeProfile:
    """Generate a validated employee profile."""

    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": "Generate employee data as JSON."},
            {"role": "user", "content": "Create profile for a Finance employee"}
        ],
        response_format={"type": "json_object"},
        temperature=0.7
    )

    try:
        raw_json = response.choices[0].message.content
        profile_data = json.loads(raw_json)

        # Pydantic validates the data
        return EmployeeProfile(**profile_data)

    except ValidationError as e:
        print(f"Validation error: {e}")
        raise
```

### Catching Invalid Data

Pydantic prevents bad data from entering the system:

```python
def demonstrate_validation():
    """Show Pydantic catching invalid data."""

    invalid_data = {
        "name": "Alex Turner",
        "employee_id": "INVALID123",  # Wrong format
        "department": "Operations",
        "email": "not-an-email",       # Invalid email
        "tenure_years": 150            # Exceeds max of 60
    }

    try:
        employee = EmployeeProfile(**invalid_data)
    except ValidationError as e:
        for error in e.errors():
            print(f"Field: {error['loc'][0]}")
            print(f"Error: {error['msg']}")
            print(f"Value: {error['input']}\n")
```

Output shows all validation failures:

```
Field: employee_id
Error: String should match pattern '^E\d{6}$'
Value: INVALID123

Field: email
Error: String should match pattern '^[^@]+@[^@]+\.[^@]+$'
Value: not-an-email

Field: tenure_years
Error: Input should be less than or equal to 60
Value: 150
```

## Financial Data Models

Let's build practical Pydantic models for financial applications:

### Trade Order Model

```python
from decimal import Decimal
from typing import Optional

class TradeOrder(BaseModel):
    """Validated trade order structure."""
    order_id: str = Field(..., pattern=r'^ORD-\d{12}$')
    symbol: str = Field(..., min_length=1, max_length=10)
    side: Literal["buy", "sell"]
    order_type: Literal["market", "limit", "stop", "stop_limit"]
    quantity: int = Field(..., gt=0, le=1000000)
    price: Optional[float] = Field(None, gt=0)
    stop_price: Optional[float] = Field(None, gt=0)
    time_in_force: Literal["day", "gtc", "ioc", "fok"] = "day"

    @validator('price')
    def limit_orders_need_price(cls, v, values):
        if values.get('order_type') in ['limit', 'stop_limit'] and v is None:
            raise ValueError('Limit orders require a price')
        return v

    @validator('stop_price')
    def stop_orders_need_stop_price(cls, v, values):
        if values.get('order_type') in ['stop', 'stop_limit'] and v is None:
            raise ValueError('Stop orders require a stop price')
        return v
```

### Risk Assessment Model

```python
class RiskAssessment(BaseModel):
    """Risk analysis output structure."""
    transaction_id: str
    risk_score: float = Field(..., ge=0, le=100)
    risk_level: Literal["low", "medium", "high", "critical"]
    factors: List[str]
    recommendation: Literal["approve", "review", "reject"]
    confidence: float = Field(..., ge=0, le=1)

    @validator('recommendation')
    def recommendation_matches_risk(cls, v, values):
        risk_level = values.get('risk_level')
        if risk_level == 'critical' and v == 'approve':
            raise ValueError('Cannot approve critical risk transactions')
        return v
```

### SWIFT Message Model

```python
class SWIFTMessage(BaseModel):
    """Validated SWIFT message structure."""
    message_type: Literal["MT103", "MT202", "MT940", "MT950"]
    sender_bic: str = Field(..., pattern=r'^[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}([A-Z0-9]{3})?$')
    receiver_bic: str = Field(..., pattern=r'^[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}([A-Z0-9]{3})?$')
    reference: str = Field(..., max_length=16)
    amount: float = Field(..., gt=0, le=999999999.99)
    currency: str = Field(..., pattern=r'^[A-Z]{3}$')
    value_date: datetime
    ordering_customer: Optional[str] = None
    beneficiary: Optional[str] = None

    def get_sender_country(self) -> str:
        """Extract country code from sender BIC."""
        return self.sender_bic[4:6]

    def get_receiver_country(self) -> str:
        """Extract country code from receiver BIC."""
        return self.receiver_bic[4:6]
```

## Combining Tools and Structured Outputs

The most powerful pattern combines tool calling with Pydantic validation:

```python
class AnalysisResult(BaseModel):
    """Structured analysis result."""
    investment: float
    profit: float
    years: int
    total_roi_percent: float
    annualized_roi_percent: float
    recommendation: Literal["strong_buy", "buy", "hold", "sell", "strong_sell"]
    confidence: float = Field(..., ge=0, le=1)
    reasoning: str

def analyze_investment_structured(
    investment: float,
    profit: float,
    years: int
) -> AnalysisResult:
    """Perform analysis with validated structured output."""

    # Step 1: Execute calculation tool
    calc_result = calculate_roi(investment, profit, years)

    # Step 2: Get LLM analysis with structured output
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": SYSTEM},
            {"role": "user", "content": f"""
                Analyze this ROI calculation and provide recommendation:
                {json.dumps(calc_result)}

                Return JSON with: recommendation, confidence, reasoning
            """}
        ],
        response_format={"type": "json_object"},
        temperature=0.2
    )

    # Step 3: Parse and validate
    analysis_data = json.loads(response.choices[0].message.content)

    return AnalysisResult(
        **calc_result,
        recommendation=analysis_data["recommendation"],
        confidence=analysis_data["confidence"],
        reasoning=analysis_data["reasoning"]
    )
```

## Takeaways

1. **Tools transform agents** from passive responders into active problem-solvers that can perform precise calculations and access real systems

2. **Function calling** provides a formal mechanism for LLMs to invoke external functions with proper argument handling

3. **Three-step tool flow**: Model requests tool → Backend executes → Model incorporates result into response

4. **Structured outputs** with `response_format={"type": "json_object"}` guarantee valid JSON responses

5. **Pydantic validation** catches invalid data before it enters your system, with detailed error reporting

6. **Financial models benefit** from strict typing - trade orders, risk assessments, and SWIFT messages all require precise data formats

7. **Combine both patterns** for maximum reliability: tools for precise execution, Pydantic for validated outputs

---

*This is the eighth post in my Applied Agentic AI for Finance series. Next: [State and Memory for Trading Agents](https://vanducng.dev/2026/01/15/State-and-Memory-for-Trading-Agents/) where we'll explore how agents maintain context across trading sessions.*
