Target Specific CVM Filing Sections in AI Agent Pipelines
AI agents that analyze Brazilian equities fail when they treat CVM filings as single documents. A FRE for one company can be 70+ files; a DFP bundle separates statements, notes, and management reports. Pulling everything into context wastes tokens, slows extraction, and buries the answer in irrelevant pages.
apicvm gives agents a section-aware tool layer: resolve ticker, filter by document name, download or extract only the relevant section, return markdown with page numbers for citation.
Who this is for
- Agent builders adding Brazil coverage to research assistants
- RAG pipelines indexing CVM text by topic (risk, governance, footnotes)
- Quant and credit teams automating issuer monitoring with LLM summarization
- Fintech products exposing "ask about this company's risk factors" features
The problem
Generic filing tools force agents into a bad loop:
- List all documents for
type=FRE→ 70 candidates - Guess which file contains risk factors
- Extract full PDF → 40 pages of governance prose the user did not ask for
- Stuff context window → hallucination risk rises
Section-aware agents instead map user intent → section name prefix → document ID → targeted extraction.
Section intent mapping
Define a lookup table from agent intents to CVM catalog name prefixes:
| Agent intent | type |
name prefix examples |
|---|---|---|
| Risk factors | FRE | DescricaoFatoresRisco, DescricaoRiscosMercado |
| Internal controls | FRE | DescricaoControlesInternos |
| Board governance | FRE | InformacoesConselhoAdm |
| Executive pay | FRE | PoliticaPraticaRemuneracao, PlanoRemuneracaoBaseadoAcoes |
| Financial notes | DFP / ITR | Notas |
| Management narrative | DFP / ITR | Relatório da Administração |
| Legal contingencies | FRE | Processos (varies — discover via list) |
The agent selects prefixes based on the user's question, not the full filing type.
Agent tool design
Expose three tools to your LLM:
resolve_company(ticker) → company metadata
list_filing_sections(ticker, type, year, name_prefix) → [{id, name, dateRef}]
extract_section(document_id) → async; pages arrive via callback
Tool 2: list_filing_sections
import os, requests
BASE = os.environ["APICVM_URL"]
H = {"Authorization": f"Bearer {os.environ['APICVM_KEY']}"}
INTENT_TO_SECTIONS = {
"risk": ["DescricaoFatoresRisco", "Descricao5PrincipaisFatoresRisco", "DescricaoRiscosMercado"],
"governance": ["InformacoesConselhoAdm", "DescricaoControlesInternos"],
"compensation": ["PoliticaPraticaRemuneracao", "PlanoRemuneracaoBaseadoAcoes"],
"footnotes": ["Notas"],
"mda": ["Relatório da Administração", "Comentário"],
}
def list_filing_sections(ticker: str, intent: str, doc_type: str, year: int) -> list[dict]:
prefixes = INTENT_TO_SECTIONS.get(intent, [])
results = []
for prefix in prefixes:
resp = requests.get(
f"{BASE}/v1/documents",
params={"ticker": ticker, "type": doc_type, "year": year, "name": prefix, "perPage": 10},
headers=H,
)
resp.raise_for_status()
results.extend(resp.json()["data"])
return results
Return compact JSON to the agent — id, name, type, dateRef only. Let the LLM pick the best match or ask the user to disambiguate.
Tool 3: extract_section
def extract_section(document_id: str, callback_url: str) -> dict:
resp = requests.post(
f"{BASE}/v1/document-text-extractions",
json={"callback_url": callback_url, "document": {"id": document_id}},
headers=H,
)
resp.raise_for_status()
return resp.json() # {"id": "42", "status": "queued", "document_id": "..."}
Your callback handler stores pages and signals the agent when extraction completes:
{
"status": "success",
"document_id": "...",
"page": { "number": 3, "markdown": "..." },
"current_page": 3,
"total_pages": 12,
"is_last_page": false
}
Example agent turn
User: "What are Vale's main commodity risk disclosures in the latest FRE?"
Agent reasoning:
resolve_company("VALE3")→ confirmedlist_filing_sections("VALE3", intent="risk", doc_type="FRE", year=2024)- Pick
DescricaoFatoresRisco-4_1document ID extract_section(id)→ wait for callback pages- Answer with quotes and page numbers from markdown
User: "Compare RADL3 footnotes on tax contingencies vs the management report."
Agent reasoning:
list_filing_sections("RADL3", intent="footnotes", doc_type="DFP", year=2024)list_filing_sections("RADL3", intent="mda", doc_type="DFP", year=2024)- Extract both sections (respect daily extraction quota)
- Cross-reference amounts and narrative
Token and cost optimization
Section filtering reduces agent cost materially:
| Approach | Typical pages extracted | Context size |
|---|---|---|
| Full FRE bundle | 200–500+ | Too large for single prompt |
| Risk sections only | 10–30 | Fits structured analysis |
| DFP notes only | 20–60 | Footnote Q&A viable |
| Single governance section | 5–15 | Fast governance checks |
Combine with retrieval: index extracted section markdown in a vector store keyed by (ticker, type, year, section_name).
Guardrails for agent builders
- Always cite page numbers from callback payloads — apicvm markdown maps to source PDF pages
- Disambiguate when
meta.total > 1— ask user or pick latestdateRef - Do not invent section names — discover via list calls when intent mapping returns empty
- State extraction limits — Pro plan: 1 markdown extraction per API key per UTC day; batch overnight or upgrade for Scale
- Async by design — agent must handle waiting/polling via your callback store, not synchronous HTTP
Current limitations
- No semantic section search — agents must know CVM
nameprefixes or discover them via list +searchparam - No page-level classification in API — within a section PDF, your agent still parses markdown structure
- Callback infrastructure required — extraction cannot complete inside a single synchronous tool call
- Student plan — markdown extraction not available (
403 FORBIDDEN); agents need Pro or above - Corpus gaps — agent should handle empty list results gracefully
Next steps
Ready to integrate?
Get an API key and start querying Brazilian CVM filings programmatically.