CoolFace
Apppublic

VTdevelops/bond-text-extraction

sourceHugging Faceupdated 5mo agoView on Hugging Face
0likes
llm_client.py145 linesDownload Raw Back to text_extraction
1from __future__ import annotations2 3import json4from typing import Sequence5 6from openai import OpenAI7from openai.types.responses import Response8 9from .models import BondRecord10 11BOND_FIELD_DESCRIPTIONS: dict[str, str] = {12    "status": "Current status of the instrument (e.g. Active, Closed, In Preparation).",13    "effective_from": "Date from which the issuance or terms become effective.",14    "isin": "International Securities Identification Number for the bond.",15    "valor": "Valor or Swiss security identifier, if disclosed.",16    "denomination": "Minimum denomination or face value per security.",17    "issue_size": "Total issue size or aggregate nominal amount.",18    "legal_venue": "Legal venue or governing law associated with the issuance.",19    "security_ranking": "Seniority or ranking of the security (e.g. Senior Unsecured).",20    "issue_price_rate": "Issue price expressed as a percentage of par or nominal value.",21    "jurisdiction_country": "Jurisdiction or country relevant for issuance or taxation.",22    "security_category": "Security category or classification used in the document.",23    "collateralization": "Description of whether and how the security is collateralised.",24    "callable_putable": "Details on call or put features, including schedules.",25    "prolongable": "Information on whether the bond can be prolonged or extended.",26    "amortizable": "Information on whether principal amortises over time.",27    "increasable": "Whether the issuer may increase the issuance amount after launch.",28    "issue_date": "Issue date stated in the term sheet.",29    "payment_date": "Initial payment or settlement date.",30    "redemption_date": "Redemption or maturity date.",31    "coupon_frequency": "Coupon payment frequency (e.g. Semi-annual).",32    "annual_rate": "Annual coupon or yield rate as described.",33    "income_begin": "Income accrual start date.",34    "first_payment_date": "Date of first coupon or interest payment.",35    "day_count_method": "Day count convention applied (e.g. 30/360, ACT/ACT).",36    "earning_specification": "Narrative describing how earnings are calculated/disbursed.",37    "interest_rate": "Interest rate description, including structure or qualifiers used in the document.",38    "rating": "Credit rating or outlook for the bond or issuer.",39}40 41 42_RESPONSE_FORMAT = {43    "type": "json_schema",44    "name": "bond_information_schema",45    "strict": True,46    "schema": {47        "type": "object",48        "properties": {49            "bonds": {50                "type": "array",51                "items": {52                    "type": "object",53                    "properties": {54                        field: {55                            "type": "string",56                            "description": description,57                        }58                        for field, description in BOND_FIELD_DESCRIPTIONS.items()59                    },60                    "required": list(BOND_FIELD_DESCRIPTIONS.keys()),61                    "additionalProperties": False,62                },63            }64        },65        "required": ["bonds"],66        "additionalProperties": False,67    },68}69 70_SYSTEM_PROMPT = (71    "You extract comprehensive bond information from financial documents. "72    "Capture the wording exactly as written when possible, without inventing data. "73    "If a field is not present, return an empty string for that entry."74)75 76 77class BondInfoExtractor:78    """Wrapper around the OpenAI client to retrieve bond information."""79 80    def __init__(self, model: str = "gpt-4.1-mini", client: OpenAI | None = None) -> None:81        self.model = model82        self.client = client or OpenAI()83 84    def extract_bonds(85        self,86        documents: Sequence[str],87        extra_instructions: str | None = None,88    ) -> list[BondRecord]:89        """Run the LLM over *documents* and parse the resulting bond records."""90 91        if not documents:92            return []93 94        response = self._run_request(documents, extra_instructions)95        raw_payload = self._extract_json_payload(response)96        bonds = raw_payload.get("bonds", [])97 98        records: list[BondRecord] = []99        for item in bonds:100            payload: dict[str, str] = {}101            for field_info in BondRecord.__dataclass_fields__.values():102                raw_value = item.get(field_info.name, "")103                payload[field_info.name] = str(raw_value).strip() if raw_value is not None else ""104            if not any(payload.values()):105                continue106            records.append(BondRecord(**payload))107 108        return records109 110    def _run_request(111        self,112        documents: Sequence[str],113        extra_instructions: str | None,114    ) -> Response:115        user_sections = []116        for index, doc in enumerate(documents, start=1):117            user_sections.append(f"Document {index}:\n{doc.strip()}")118 119        user_prompt = "\n\n".join(user_sections)120        if extra_instructions:121            user_prompt += f"\n\nAdditional guidance: {extra_instructions.strip()}"122 123        return self.client.responses.create(124            model=self.model,125            input=[126                {"role": "system", "content": [127                    {"type": "input_text", "text": _SYSTEM_PROMPT},128                ]},129                {"role": "user", "content": [130                    {"type": "input_text", "text": user_prompt},131                ]},132            ],133            text={"format": _RESPONSE_FORMAT},134        )135 136    @staticmethod137    def _extract_json_payload(response: Response) -> dict:138        for output in response.output or []:139            for content in output.content or []:140                if content.type == "output_text":141                    return json.loads(content.text)142                if content.type == "json_schema":143                    return json.loads(content.json_schema.value)144        raise ValueError("Unexpected response structure from OpenAI Agents API")145