LangChain Tools for Brazil CVM Filings

Agents that research Brazilian equities need regulated source documents — DFP annual statements, ITR quarterly reports, FRE reference forms. If you search for LangChain CVM filings patterns, you will find generic web tools, not Brazil-specific regulatory APIs.

apicvm exposes REST endpoints for resolve, list, and download. There is no official apicvm LangChain SDK — this guide shows how to wrap HTTP calls as LangChain tools so your agent can fetch real CVM filings.

The problem

LangChain agents call tools in a loop: plan → act → observe. Brazilian regulatory data breaks that loop when:

  • Tickers like PETR4 do not map cleanly to CVM bulk CSV dumps.
  • PDFs are binary — LLMs need text or structured excerpts.
  • Filings split into dozens of files per year (especially FRE).

You need tools that return actionable JSON or file paths, not HTML scrape results.

Architecture

User question
    → LangChain agent (ReAct / tool-calling)
        → Custom HTTP tools (your code)
            → apicvm REST API (/v1/*)
                → CVM corpus (Hold ingestion)

Each tool is a thin wrapper around requests (or httpx) with your API key in headers. LangChain does not ship apicvm integration — you own the tool definitions and error mapping.

Setup

import os
import requests

BASE = os.environ["APICVM_URL"].rstrip("/")
HEADERS = {"Authorization": f"Bearer {os.environ['APICVM_KEY']}"}

def apicvm_get(path: str, **params):
    r = requests.get(f"{BASE}{path}", headers=HEADERS, params=params, timeout=30)
    r.raise_for_status()
    return r.json()

Store APICVM_KEY in environment variables. Never embed keys in agent prompts or committed source.

Tool 1: resolve_company

Maps ticker, CNPJ, or name to a single company record.

from langchain_core.tools import tool

@tool
def resolve_company(query: str, by: str = "ticker") -> dict:
    """Resolve a Brazilian public company by ticker (e.g. PETR4), CNPJ, or name."""
    return apicvm_get("/v1/companies/resolve", query=query, by=by)

Handle 409 AMBIGUOUS_RESULT in your agent wrapper — return details.candidates so the model can disambiguate.

Tool 2: list_documents

Lists filings filtered by ticker, type, and year.

@tool
def list_documents(
    ticker: str,
    doc_type: str,
    year: int,
    name: str | None = None,
    per_page: int = 10,
) -> dict:
    """List CVM filings for a ticker. doc_type is DFP, ITR, or FRE."""
    params = {"ticker": ticker, "type": doc_type, "year": year, "perPage": per_page}
    if name:
        params["name"] = name
    return apicvm_get("/v1/documents", **params)

The agent must pick a document.id from data[] — the API does not choose automatically.

Example agent trace:

  1. User: "Find PETR4 latest DFP and summarize debt disclosures."
  2. Agent calls list_documents("PETR4", "DFP", 2024).
  3. Agent selects a notes or financial-statement id based on name field.

For FRE section filtering, pass name= — see filter CVM filings by section name.

Tool 3: download_filing

Downloads the original PDF to a local path for downstream parsing or attachment.

@tool
def download_filing(document_id: str, output_path: str) -> str:
    """Download the original CVM filing PDF. Returns the saved file path."""
    r = requests.get(
        f"{BASE}/v1/documents/{document_id}/file",
        headers=HEADERS,
        timeout=120,
    )
    r.raise_for_status()
    with open(output_path, "wb") as f:
        f.write(r.content)
    return output_path

Download is free — no extraction credits consumed.

Tool 4 (optional): enqueue_extraction

When the agent needs markdown text (not raw PDF bytes), enqueue async extraction. Pro plan only — Student keys receive 403 FORBIDDEN.

@tool
def enqueue_extraction(document_id: str, callback_url: str) -> dict:
    """Enqueue page-level markdown extraction. Progress arrives via callback POSTs."""
    r = requests.post(
        f"{BASE}/v1/document-text-extractions",
        headers={**HEADERS, "Content-Type": "application/json"},
        json={"callback_url": callback_url, "document": {"id": document_id}},
        timeout=30,
    )
    r.raise_for_status()
    return r.json()  # 202: id, status, document_id

Extraction debits N credits (N = page count) in the prepare job after 202. Insufficient balance → callback error_code: EXTRACTION_CREDITS_EXCEEDED. Cache hits still cost N credits.

Callback challenge for agents

LangChain agents expect synchronous tool results. apicvm extraction is async only — one callback POST per page, no job status HTTP endpoint in v1.

Practical patterns:

  1. Webhook + store — your callback handler writes pages to Redis/Postgres; a separate get_extraction_pages(document_id) tool reads stored markdown.
  2. Pre-extract offline — batch-enqueue extractions outside the agent loop; agents read cached text from your vector store.
  3. Demo markdown (VALE3 only)GET /v1/demo/documents/:id/markdown returns cached pages without a key, limited to FRE 2025 risk factors.

Do not block the agent loop waiting for callbacks. See async CVM PDF extraction callbacks.

Wiring tools into an agent

Conceptual ReAct setup with LangChain:

from langchain.agents import create_react_agent, AgentExecutor
from langchain_openai import ChatOpenAI

tools = [resolve_company, list_documents, download_filing]
llm = ChatOpenAI(model="gpt-4o")

agent = create_react_agent(llm, tools, prompt=your_prompt)
executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

executor.invoke({
    "input": "Resolve PETR4, list DFP 2024 filings, and report the document names."
})

Adjust imports to your LangChain version (langgraph tool nodes work the same way — HTTP wrappers stay identical).

Return compact JSON from tools (truncate long lists) so you do not blow the context window with full API responses.

RAG integration

For retrieval pipelines, combine these tools with chunking and embedding:

  1. list_documents → pick document.id
  2. enqueue_extraction → collect markdown pages via callback
  3. Chunk → embed → store in vector DB
  4. Agent answers with citations to page numbers

Full walkthrough: Build a RAG pipeline over Brazil CVM filings.

Use case context: Brazil filings for AI agents.

Current limitations

  • No official LangChain package — maintain your own tool wrappers; API contract is HTTP-only.
  • Async extraction — agents cannot synchronously "get all text" in one tool call on authenticated routes.
  • Corpus gaps — not every issuer or period is ingested; tools should surface 404 clearly to the model.
  • Credit costs — large FRE/DFP extractions consume many credits; budget before autonomous agent loops.
  • Student plan — no extraction tool without upgrading to Pro.

Next steps

Ready to integrate?

Get an API key and start querying Brazilian CVM filings programmatically.